Как вы используете 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")