Как вы используете scalaz.WriterT для регистрации?
Как вы используете scalaz.WriterT для входа в выражение for?
Ответ 1
О монадных трансформаторах
Это очень короткое введение. Вы можете найти более подробную информацию о haskellwiki или этом большом слайде на @jrwest.
Монады не сочиняют, что означает, что если у вас есть монада A[_]
и монада B[_]
, тогда A[B[_]]
невозможно получить автоматически. Однако в большинстве случаев это может быть достигнуто за счет наличия так называемого монадного трансформатора для данной монады.
Если у нас есть monad transformer BT
для monad B
, то мы можем составить новую монаду A[B[_]]
для любой монады A
. Правильно, используя BT
, мы можем поместить B
внутрь A
.
Использование трансформатора Монады в скалясе
Следующее предполагает скаляз 7, поскольку, честно говоря, я не использовал монадные трансформаторы со сказазом 6.
Монадный трансформатор MT
принимает два типа параметров, первый - это оболочка (внешняя), вторая - фактический тип данных в нижней части стека монады. Примечание. Может потребоваться больше параметров типа, но они не связаны с трансформаторностью, а скорее специфичны для данной монады (например, для журнала Writer
или типа ошибки Validation
).
Итак, если у нас есть List[Option[A]]
, который мы хотели бы рассматривать как единственную составленную монаду, тогда нам нужно OptionT[List, A]
. Если мы имеем Option[List[A]]
, нам нужно ListT[Option, A]
.
Как туда добраться? Если у нас есть значение без трансформатора, мы можем просто обернуть его с помощью MT.apply
, чтобы получить значение внутри трансформатора. Чтобы вернуться из преобразованной формы в нормальную, мы обычно вызываем .run
на преобразованное значение.
Итак, val a: OptionT[List, Int] = OptionT[List, Int](List(some(1))
и val b: List[Option[Int]] = a.run
- это те же данные, просто представление отличается.
Было высказано Тони Моррисом, что лучше всего перейти в преобразованную версию как можно раньше и использовать это как можно дольше.
Примечание. Составление нескольких монадов с использованием трансформаторов дает стек трансформатора с типами, противоположным порядку, как обычный тип данных. Таким образом, нормальный List[Option[Validation[E, A]]]
будет выглядеть примерно как type ListOptionValidation[+E, +A] = ValidationT[({type l[+a] = OptionT[List, a]})#l, E, A]
Обновление: по сравнению с scalaz 7.0.0-M2, Validation
(правильно) не является монадом, поэтому ValidationT
не существует. Вместо этого используйте EitherT
.
Использование WriterT для ведения журнала
В соответствии с вашими потребностями вы можете использовать WriterT
без какой-либо конкретной внешней монады (в этом случае в фоновом режиме она будет использовать монаду Id
, которая ничего не делает), или может помещать журнал внутри monad, или поместите монаду в журнал.
Первый случай, простая запись
import scalaz.{Writer}
import scalaz.std.list.listMonoid
import scalaz._
def calc1 = Writer(List("doing calc"), 11)
def calc2 = Writer(List("doing other"), 22)
val r = for {
a <- calc1
b <- calc2
} yield {
a + b
}
r.run should be_== (List("doing calc", "doing other"), 33)
Мы импортируем экземпляр listMonoid
, так как он также предоставляет экземпляр Semigroup[List]
. Это необходимо, так как WriterT
нужен тип журнала для полугруппы, чтобы иметь возможность комбинировать значения журнала.
Второй случай, вход в монаду
Здесь мы выбрали монаду Option
для простоты.
import scalaz.{Writer, WriterT}
import scalaz.std.list.listMonoid
import scalaz.std.option.optionInstance
import scalaz.syntax.pointed._
def calc1 = WriterT((List("doing calc") -> 11).point[Option])
def calc2 = WriterT((List("doing other") -> 22).point[Option])
val r = for {
a <- calc1
b <- calc2
} yield {
a + b
}
r.run should be_== (Some(List("doing calc", "doing other"), 33))
При таком подходе, поскольку ведение журнала находится внутри монады Option
, если какая-либо из связанных опций None
, мы просто получим результат None
без каких-либо журналов.
Примечание: x.point[Option]
то же самое действует как Some(x)
, но может помочь лучше обобщить код. На данный момент это не смертоносное дело.
Третий вариант, запись вне монады
import scalaz.{Writer, OptionT}
import scalaz.std.list.listMonoid
import scalaz.std.option.optionInstance
import scalaz.syntax.pointed._
type Logger[+A] = WriterT[scalaz.Id.Id, List[String], A]
def calc1 = OptionT[Logger, Int](Writer(List("doing calc"), Some(11): Option[Int]))
def calc2 = OptionT[Logger, Int](Writer(List("doing other"), None: Option[Int]))
val r = for {
a <- calc1
b <- calc2
} yield {
a + b
}
r.run.run should be_== (List("doing calc", "doing other") -> None)
Здесь мы используем OptionT
для размещения монады Option
внутри Writer
. Одним из расчетов является None
, чтобы показать, что даже в этом случае сохраняются журналы.
Заключительные замечания
В этих примерах List[String]
использовался как тип журнала. Однако использование String
вряд ли когда-либо является лучшим способом, только некоторые конвенции принудительно на нас, запустив фреймворки. Было бы лучше определить пользовательский лог файл ADT, например, и, если необходимо, вывести его в строку как можно позже. Таким образом, вы можете сериализовать журнал ADT и легко проанализировать его позже программным способом (вместо синтаксического анализа строк).
WriterT
имеет множество полезных методов для работы, чтобы облегчить ведение журнала, проверьте источник. Например, с учетом w: WriterT[...]
вы можете добавить новую запись в журнал с помощью w :++> List("other event")
или даже зарегистрироваться с использованием текущего значения с помощью w :++>> ((v) => List("the result is " + v))
и т.д.
В примерах много явных и длинных кодов (типов, вызовов). Как всегда, это для ясности, рефакторинг их в вашем коде путем извлечения общих типов и операций.
Ответ 2
type OptionLogger[A] = WriterT[Option, NonEmptyList[String], A]
val two: OptionLogger[Int] = WriterT.put(2.some)("The number two".pure[NonEmptyList])
val hundred: OptionLogger[Int] = WriterT.put(100.some)("One hundred".pure[NonEmptyList])
val twoHundred = for {
a <- two
b <- hundred
} yield a * b
twoHundred.value must be equalTo(200.some)
val log = twoHundred.written map { _.list } getOrElse List() mkString(" ")
log must be equalTo("The number two One hundred")