Ответ 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
. Скорее, подход, который я дал, более подходит для регистрации в производственном коде, где предполагается, что в вашем коде не будет никаких противоречий или ошибок.
Заключение
Надеюсь, это поможет!
Здесь - связанный пост по теме.