Как добавить трассировку в понимании «для»? - PullRequest
25 голосов
/ 24 февраля 2010

Для трассировки журнала внутри 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 = кажется неуклюжим. Есть ли способ чище?

Ответы [ 5 ]

71 голосов
/ 02 февраля 2011

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

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

Во-первых, что такое понимание? Понимание - это (примерно достаточно для наших целей) понимание монады, но с другим именем. Это обычная тема; C # имеет 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. Вопрос, который у вас есть, по сути, как нам добавить logging поддержку этих значений?

Тип данных 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 (пусто, x) == x
  • Левый идентификатор: append (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 значений для своего журнала. Звучит достаточно разумно? Тем не менее, есть пара замечаний:

  1. На практике мы не будем использовать List из-за нежелательной алгоритмической сложности его append. Скорее мы могли бы использовать основанную на пальце последовательность или что-то еще с более быстрой вставкой в конце операции .
  2. 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

Точно так же, как не записываемые в журнал значения только для понимания!

WriterTMonad Transformer

Правильно, так как же нам добавить эту возможность ведения журнала в наше существующее для понимания?Здесь вам нужен монадный трансформатор WriterT.Опять же, мы специализируем его на 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))
      }))
  }

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

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

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

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

Критика

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

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

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

Заключение

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

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

21 голосов
/ 24 февраля 2010

Вы всегда можете определить свою собственную 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

РЕДАКТИРОВАТЬ: В ответ на ваш комментарий, да, вы можете написать 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)

15 голосов
/ 24 февраля 2010

Сколько бы это ни стоило, так как задание является фиктивным, вы можете заменить a на _:

for { 
  outer <- ll  // ; // semi-colon needed on Scala 2.7
  _ = Console.println(outer)   // dummy assignment makes it compile 
  inner <- outer 
} yield inner 
1 голос
/ 25 февраля 2010

Ответ Флавиу вдохновил меня попробовать сыграть со следствиями. Идея состоит в том, чтобы увидеть, выглядит ли трасса лучше с «трассировкой» справа от линии:

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's 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
    }  
  }  
}
0 голосов
/ 17 марта 2019

Начиная с Scala 2.13, операция объединения tap включена в стандартную библиотеку и может использоваться с минимальной навязчивостью везде, где нам нужно напечатать какое-то промежуточное состояние конвейера:

import util.chaining._

// val lists = List(List(1, 2), List(1))
for {
  outer <- lists
  inner <- outer.tap(println)
} yield inner
// List(2, 4, 6)
// List(4, 8, 12)
// ls: List[Int] = List(4, 8, 12)

Операция tap применяет побочный эффект (в данном случае println) к значению (в данном случае список outer), возвращая это значение без изменений:

def tap [U] (f: (A) => U): A

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...