Как добавить трассировку в понимании "для"?
Для отслеживания журнала внутри 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 =
немного неловко Есть ли более чистый способ?
4 ответа
Вы всегда можете определить свой собственный 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
)
Краткий ответ на ваш вопрос: 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. Вопрос, который у вас есть, по сути, как мы можем добавить поддержкурегистрации в эти значения?
Тип данных 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)
- Правильная идентификация: добавление (пусто, х) == х
- Левая идентичность: добавьте (x, пусто) == x
В качестве примечания, это также те же законы, что реализацииMonad
интерфейс должен следовать, но я оставил их, чтобы сохранить путаницу и остаться на месте регистрации.
Есть много примеров реализации этогоMonoid
интерфейс, одним из которых является список:
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)
... теперь давайте посмотрим на это в действии. Код ниже аналогичен для понимания. Однако вместо того, чтобы вытягивать значения и называть их слева от<-
, мыотображаем значения и называем их справа. Мы используем явные вызовы функций, которые мы определили вместо для понимания:
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
Точно так же, как не записываемые в журнал значения только для понимания!
WriterT Монад Трансформер
Правильно, так, как мы можем добавить эту возможность регистрации к нашему существующему для понимания? Это где вам нужно 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))
}))
}
Смысл этой реализации монады в том, что вы можете прикрепить логирование к любому значению M
до тех пор, пока есть Monad
за M
, Другими словами, именно так вы можете "добавить трассировку для понимания". Обработка добавляемых значений журнала будет позаботиться автоматически Monad
реализация.
В целях объяснения мы отклонились от того, как такая библиотека будет реализована для практического использования. Например, когда мы используем Monad
реализация для ListWriterT
мы, вероятно, будем настаивать на использовании для понимания. Тем не менее, мы не осуществили прямо (или косвенно) flatMap
или же map
методы, поэтому мы не можем сделать это в том виде, в каком оно есть.
Тем не менее, я надеюсь, что это объяснение дало понять, как WriterT
монадный трансформатор решит вашу проблему.
Теперь кратко рассмотрим достоинства и возможные недостатки этого подхода.
рецензия
Хотя часть приведенного выше кода может быть довольно абстрактной и даже шумной, в ней заложена алгебраическая концепция ведения журнала при вычислении значения. Библиотека, которая была специально разработана для этого в практическом смысле, максимально облегчила бы нагрузку на клиентский код. По совпадению, я реализовал такую библиотеку для Scala несколько лет назад, когда работал над коммерческим проектом.
Смысл ведения журнала таким способом состоит в том, чтобы отделить типичный побочный эффект (например, печать или запись в файл журнала) от вычисления значения со связанным журналом и обработать моноидальное свойство ведения журнала автоматически для вызывающего клиента. В конечном счете, это разделение приводит к коду, который намного легче читать и рассуждать (верьте, хотите нет, несмотря на некоторый синтаксический шум) и менее подвержен ошибкам. Кроме того, он помогает в повторном использовании кода, комбинируя абстрактные функции высокого уровня, чтобы создавать все больше и больше специализированных функций, пока в конечном итоге вы не окажетесь на уровне своего конкретного приложения.
Недостатком этого подхода является то, что он не поддается аварийному завершению программы. То есть, если вы, как программист, пытаетесь разрешить аргумент с помощью средства проверки типов или среды выполнения, то вы, вероятно, захотите использовать отладочные точки останова или print
заявления. Скорее, предложенный мной подход больше подходит для входа в производственный код, где предполагается, что в вашем коде нет противоречий или ошибок.
Заключение
Надеюсь, это поможет!
Вот соответствующий пост по теме.
Для чего бы это ни стоило, так как задание является пустышкой, вы можете заменить a
с _
:
for {
outer <- ll // ; // semi-colon needed on Scala 2.7
_ = Console.println(outer) // dummy assignment makes it compile
inner <- outer
} yield inner
Запуск 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
list), возвращая это значение нетронутым:
def tap [U](f: (A) => U):A
Ответ Флавиу вдохновил меня попробовать сыграть со следствиями. Идея состоит в том, чтобы увидеть, выглядит ли трассировка лучше с "трассировкой" дальше вправо на линии:
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
}
}
}