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