Как добавить трассировку в "понимание"?

Для отслеживания журнала внутри a for, я использовал фиктивное назначение следующим образом:

val ll = List(List(1,2),List(1))            

for {
  outer <- ll 
  a = Console.println(outer)   // Dummy assignment makes it compile
  inner <- outer
} yield inner

Бит a = кажется неудобным. Есть ли более чистый способ?

Ответ 1

Вы всегда можете определить свою собственную функцию trace:

def trace[T](x: T) = {
  println(x) // or your favourite logging framework :)
  x
}

Тогда для понимания будет выглядеть:

for { 
  outer <- ll
  inner <- trace(outer)
} yield inner

В качестве альтернативы, если вы хотите напечатать дополнительную информацию, вы можете определить trace следующим образом:

def trace[T](message: String, x: T) = {
  println(message)
  x
}

и для понимания будет выглядеть так:

for { 
  outer <- ll
  inner <- trace("Value: " + outer, outer)
} yield inner

EDIT: В ответ на ваш комментарий, да, вы можете написать trace, чтобы он действовал справа от цели! Вам просто нужно использовать немного неявного обмана. И на самом деле, это выглядит намного лучше, чем при применении слева:).

Чтобы сделать это, вы должны сначала определить класс, который является Traceable, а затем определить неявное преобразование в этот класс:

class Traceable[A](x: A) { 
  def traced = {
    println(x)
    x
  }
}

implicit def any2Traceable[A](x: A) = new Traceable(x)

Тогда единственное, что вам нужно изменить в коде, который вы предоставили, - добавить traced в конец значения, которое вы хотите проследить. Например:

for { 
  outer <- ll
  inner <- outer traced
} yield inner

(это преобразуется компилятором Scala в outer.traced)

Ответ 2

Короткий ответ на ваш вопрос: трансформатор WriterT. Долгий ответ следует.

В следующем объяснении я собираюсь дать вам инструмент, который достигнет желаемой цели, но используя совершенно другой механизм для тех, которые уже были сформулированы. Я предложу свое краткое мнение о достоинствах различий к концу.

Во-первых, что для понимания? Для понимания (приблизительно для наших целей) понимается монада, но с другим именем. Это обычная тема; С# имеет LINQ, например.

Что такое монада?

Для наших целей объяснения (это не совсем так, но на данный момент это действительно так), монада - это любое значение для M, которое реализует следующую характеристику:

trait Monad[M[_]] {
  def flatMap[A, B](a: M[A], f: A => M[B]): M[B]
  def map[A, B](a: M[A], f: A => B): M[B]
}

То есть, если у вас есть реализация Monad для некоторого M, тогда вы можете использовать для понимания значение с типом M [A] для любого значения A.

Некоторые примеры значений M, которые соответствуют этому интерфейсу и находятся в стандартной библиотеке, это List, Option и Parser. Конечно, вы, вероятно, всегда используете для них понимание. Другими примерами могут быть ваши собственные типы данных. Например:

case class Inter[A](i: Int => A) 

... и вот реализация Monad для Inter:

val InterMonad: Monad[Inter] = new Monad[Inter] {
  def flatMap[A, B](a: Inter[A], f: A => Inter[B]) =
    Inter(n => f(a.i(n)).i(n))
  def map[A, B](a: Inter[A], f: A => B) =
    Inter(n => f(a.i(n)))
}

Есть много много больше значений для M. Вопрос, который у вас есть, по существу, , как мы добавляем поддержку ведения журналов к этим значениям?

Тип данных Writer

Тип данных Writer - это просто пара (scala.Tuple2). В этой паре мы вычисляем некоторое значение (давайте назовем его A) и сопоставим с ним другое значение (позвоним ему LOG).

// simply, a pair
case class Writer[LOG, A](log: LOG, value: A)

Когда мы вычисляем значения, мы хотим добавить значение журнала в текущий вычисленный журнал. Прежде чем мы начнем вычислять что-либо, мы хотим иметь пустой журнал. Мы можем представить эти операции (append и empty) в интерфейсе:

trait Monoid[A] {
  def append(a1: A, a2: A): A
  def empty: A
}

Существуют некоторые законы, которые должны выполняться всеми реализациями этого интерфейса:

  • Ассоциативность: append (x, append (y, z)) == append (append (x, y), z)
  • Правильная идентификация: append (empty, x) == x
  • Левая идентификация: добавить (x, пусто) == x

В качестве побочного примечания, это также те же законы, что и реализации интерфейса Monad, но я оставил их, чтобы сохранить путаницу и остаться на месте регистрации.

Существует много примеров реализации этого интерфейса Monoid, одним из которых является List:

def ListMonoid[A]: Monoid[List[A]] = new Monoid[List[A]] {
  def append(a1: List[A], a2: List[A]) = 
    a1 ::: a2
  def empty =
    Nil
}

Просто, чтобы отметить, насколько разнообразен этот Monoid интерфейс, вот еще один пример реализации:

def EndoMonoid[A]: Monoid[A => A] = new Monoid[A => A] {
  def append(a1: A => A, a2: A => A) =
    a1 compose a2
  def empty =
    a => a
}

Я понимаю, что эти обобщения могут быть немного затруднены в вашей голове, так что теперь я собираюсь сделать это, специализируя Writer, чтобы использовать List из String значений для своего журнала. Звучит достаточно разумно? Однако есть несколько замечаний:

  • На практике мы не использовали бы List из-за нежелательной алгоритмической сложности ее append. Скорее, мы можем использовать последовательность на основе пальца или что-то еще с более быстрой вставкой в ​​конце операции.
  • List[String] - всего лишь один пример реализации Monoid. Важно иметь в виду, что существует огромное количество других возможных реализаций, многие из которых не являются типами сбора. Просто помните, что все, что нам нужно, - это любой Monoid, чтобы прикрепить значение журнала.

Вот наш новый тип данных, который специализируется на Writer.

case class ListWriter[A](log: List[String], value: A)

Что в этом такого интересного? Это монада! Важно отметить, что его реализация Monad отслеживает ведение журнала для нас, что важно для нашей цели. Пусть написана реализация:

val ListWriterMonad: Monad[ListWriter] = new Monad[ListWriter] {
  def flatMap[A, B](a: ListWriter[A], f: A => ListWriter[B]) = {
    val ListWriter(log, b) = f(a.value)
    ListWriter(a.log ::: log /* Monoid.append */, b)
  }
  def map[A, B](a: ListWriter[A], f: A => B) = 
    ListWriter(a.log, f(a.value))
} 

Обратите внимание на реализацию flatMap, в которую добавляются зарегистрированные значения. Далее нам понадобятся вспомогательные функции для привязки значений журнала:

def log[A](log: String, a: A): ListWriter[A] =
  ListWriter(List(log), a)

def nolog[A](a: A): ListWriter[A] =
  ListWriter(Nil /* Monoid.empty */, a)

... теперь посмотрим его в действии. Приведенный ниже код аналогичен понятию. Однако вместо того, чтобы вытаскивать значения и называть их слева от <-, мы устанавливаем значения flatMap и назовите их справа. Мы используем явные вызовы функций, которые мы определили, а не для понимания:

val m = ListWriterMonad
val r = 
  m flatMap (log("computing an int", 42), (n: Int) =>
  m flatMap (log("adding 7",      7 + n), (o: Int) =>
  m flatMap (nolog(o + 3),                (p: Int) =>
  m map     (log("is even?", p % 2 == 0), (q: Boolean) =>
    !q))))
println("value: " + r.value)
println("LOG")
r.log foreach println

Если вы запустите этот небольшой фрагмент, вы увидите окончательное вычисленное значение и журнал, который был накоплен во время вычисления. Важно отметить, что вы можете перехватить это вычисление в любой момент и наблюдать за текущим журналом, а затем продолжить вычисление, используя ссылочно прозрачное свойство выражения и его подвыражений. Обратите внимание, что на протяжении всего вычисления вы еще не выполнили никаких побочных эффектов, и поэтому вы сохранили композиционные свойства программы.

Вы также можете реализовать map и flatMap на ListWriter, который просто скопирует реализацию Monad. Я оставлю это для вас. Это позволит вам использовать для понимания:

val r = 
  for { 
    n <- log("computing an int", 42)
    o <- log("adding 7",      7 + n)
    p <- nolog(o + 3)
    q <- log("is even?", p % 2 == 0)
  } yield !q
println("value: " + r.value)
println("LOG")
r.log foreach println

Точно так же, как значения без регистрации только для понимания!

Трансформатор Monad WriterT

Вправо, так как мы можем добавить эту способность к регистрации для нашего существующего понимания? Здесь вам понадобится трансформатор WriterT monad. Опять же, мы будем специализироваться на List для ведения журнала и для демонстрации:

// The WriterT monad transformer
case class ListWriterT[M[_], A](w: M[ListWriter[A]])

Этот тип данных добавляет ведение журнала к значениям, которые вычисляются внутри любого значения для M. Он делает это со своей собственной реализацией для Monad. К сожалению, для этого требуется приложение конструктора с частичным типом, все в порядке, за исключением Scala, это не очень хорошо. По крайней мере, это немного шумно и требует немного рук. Вот он, пожалуйста, не стесняйтесь:

def ListWriterTMonad[M[_]](m: Monad[M]): 
      Monad[({type λ[α]=ListWriterT[M, α]})#λ] =
  new Monad[({type λ[α]=ListWriterT[M, α]})#λ] {
    def flatMap[A, B](a: ListWriterT[M, A], f: A => ListWriterT[M, B]) =
      ListWriterT(
        m flatMap (a.w, (p: ListWriter[A]) =>
            p match { case ListWriter(log1, aa) => 
        m map     (f(aa).w, (q: ListWriter[B]) =>
            q match { case ListWriter(log2, bb) =>
        ListWriter(log1 ::: log2, bb)})
      }))
    def map[A, B](a: ListWriterT[M, A], f: A => B) = 
      ListWriterT(
        m map (a.w, (p: ListWriter[A]) =>
            p match { case ListWriter(log, aa) => 
        ListWriter(log, f(aa))
      }))
  }

Точка этой реализации монады заключается в том, что вы можете присоединить ведение журнала к любому значению M до тех пор, пока существует Monad для M. Другими словами, вы можете "добавить трассировку в пределах понятий". Обработка значений добавляемых журналов будет автоматически выполняться с помощью реализации Monad.

В целях пояснения мы отклонились от того, как такая библиотека будет реализована для практического использования. Например, когда мы используем реализацию Monad для ListWriterT, мы, вероятно, будем настаивать на использовании понятий. Тем не менее, мы не прямо (или косвенно) не реализовали методы flatMap или map, поэтому мы не можем делать это, как оно есть.

Тем не менее, я надеюсь, что это объяснение позволило понять, как трансформатор WriterT преобразует ваши проблемы.

Теперь, чтобы кратко рассмотреть достоинства и возможные недостатки этого подхода.

Критика

Хотя некоторые из приведенных выше кодов могут быть довольно абстрактными и даже шумными, он инкапсулирует алгебраическую концепцию ведения журнала при вычислении значения. Библиотека, которая была специально разработана для этого в практическом смысле, максимально облегчила бы нагрузку на клиентский код. Кстати, я несколько лет назад реализовал такую ​​библиотеку для Scala, когда я работал над коммерческим проектом.

Точкой ведения журнала таким образом является отделить типичный побочный эффект (например, печать или запись в файл журнала) от вычисления значения с помощью связанного журнала и обрабатывать моноидальное свойство автоматического ведения журнала для вызова клиент. В конечном счете, это разделение приводит к коду, который намного легче читать и рассуждать (верьте или нет, несмотря на некоторый синтаксический шум) и менее подвержен ошибкам. Кроме того, он помогает повторному использованию кода, комбинируя абстрактные функции высокого уровня, чтобы создавать все более специализированные функции, пока, в конце концов, вы не достигнете уровня своего конкретного приложения.

Недостатком этого подхода является то, что он не поддается сбою программы. То есть, если вы, как программист, пытаетесь разрешить аргумент с помощью проверки типов или среды выполнения, то вы, вероятно, захотите использовать контрольные точки отладки или print. Скорее, подход, который я дал, более подходит для регистрации в производственном коде, где предполагается, что в вашем коде не будет никаких противоречий или ошибок.

Заключение

Надеюсь, это поможет!

Здесь - связанный пост по теме.

Ответ 3

Для того, что бы это ни стоило, поскольку назначение является фиктивным, вы можете заменить a на _:

for { 
  outer <- ll  // ; // semi-colon needed on Scala 2.7
  _ = Console.println(outer)   // dummy assignment makes it compile 
  inner <- outer 
} yield inner 

Ответ 4

Ответ Flaviu вдохновил меня на то, чтобы попытаться сыграть с имплицитами. Идея состоит в том, чтобы увидеть, выглядит ли трассировка лучше с помощью "трассировки" справа от строки:

import Trace._

object Main {  
  def main(args:Array[String])  {
    val listList = List(List(1,2,3), List(3,4))    
    for {
      list <- trace1(listList, "lList is: %s", listList)  // trace() 
      item <- list traced("list is: %s", list)            // implicit         
    } yield item

Я также хотел попробовать смешать запись ошибок в том же понимании. Запись ошибок, похоже, лучше всего сочетается с подходами Дэниела:

    val optOpt:Option[Option[Int]] = Some(Some(1))
    for {
      opt <- optOpt;
      _ = trace2("opt found: %s", opt)   // trying Daniel suggestion
      int <- opt orElse 
        err("num not found in: %s", opt)   // together with error logging
    } yield int
  }
}

Здесь поддерживающий код для обоих экспериментов:

object Trace {
  def trace1[T](any:T, message:String, params:AnyRef*):T = {
    Console println String.format("TRA: " + message, params:_*)
    any
  }

  def trace2[T](message:String, params:AnyRef*) {
    Console println String.format("TRA: " + message, params:_*)
  }

  def err[T](message:String, params:AnyRef*):Option[T] = {
    Console println String.format("ERR: " + message, params:_*)
    None
  }

  implicit def anyRefToTraceable[T](anyRef:T):Traceable[T] = {
    new Traceable(anyRef)
  }

  class Traceable[T](val self:T) {
    def traced(message:String, params:AnyRef*):T = {
      Console println String.format("TRA: " + message, params:_*)
      self
    }  
  }  
}