Writer? Я о ней почти не знаю!
Итак, мы зарядили наш пистолет монадой Maybe, списковой монадой и монадой IO. Теперь давайте поместим в патронник монаду Writer и посмотрим, что произойдёт, когда мы выстрелим ею!
Между тем как Maybe предназначена для значений с добавленным контекстом неуспешно оканчивающихся вычислений, а список – для недетерминированных вычислений, монада Writer предусмотрена для значений, к которым присоединено другое значение, ведущее себя наподобие журнала. Монада Writer позволяет нам производить вычисления, в то же время обеспечивая слияние всех журнальных значений в одно, которое затем присоединяется к результату.
Например, мы могли бы снабдить наши значения строками, которые объясняют, что происходит, возможно, для отладочных целей. Рассмотрите функцию, которая принимает число бандитов в банде и сообщает нам, является ли эта банда крупной. Это очень простая функция:
isBigGang :: Int –> Bool
isBigGang x = x > 9
Ну а что если теперь вместо возвращения значения True или False мы хотим, чтобы функция также возвращала строку журнала, которая сообщает, что она сделала? Что ж, мы просто создаём эту строку и возвращаем её наряду с нашим значением Bool:
isBigGang :: Int –> (Bool, String)
isBigGang x = (x > 9, "Размер банды сравнён с 9.")
Так что теперь вместо того, чтобы просто вернуть значение типа Bool, мы возвращаем кортеж, первым компонентом которого является само значение, а вторым компонентом – строка, сопутствующая этому значению. Теперь у нашего значения появился некоторый добавленный контекст. Давайте опробуем функцию:
ghci> isBigGang 3
(False,"Размер банды сравнён с 9.")
ghci> isBigGang 30
(True,"Размер банды сравнён с 9.")
Пока всё нормально. Функция isBigGang принимает нормальное значение и возвращает значение с контекстом. Как мы только что увидели, передача ей нормального значения не составляет сложности. Теперь предположим, что у нас уже есть значение, у которого имеется журнальная запись, присоединённая к нему – такая как (3, "Небольшая банда.") – и мы хотим передать его функции isBigGang. Похоже, перед нами снова встаёт вопрос: если у нас есть функция, которая принимает нормальное значение и возвращает значение с контекстом, как нам взять нормальное значение с контекстом и передать его функции?
Исследуя монаду Maybe, мы создали функцию applyMaybe, которая принимала значение типа Maybe a и функцию типа a –> Maybe b и передавала это значение Maybe a в функцию, даже если функция принимает нормальное значение типа a вместо Maybe a. Она делала это, следя за контекстом, имеющимся у значений типа Maybe a, который означает, что эти значения могут быть значениями с неуспехом вычислений. Но внутри функции типа a –> Maybe b мы могли обрабатывать это значение как нормальное, потому что applyMaybe (которая позже стала функцией >>=) проверяла, являлось ли оно значением Nothing либо значением Just.
В том же духе давайте создадим функцию, которая принимает значение с присоединённым журналом, то есть значением типа (a,String), и функцию типа a –> (b,String), и передаёт это значение в функцию. Мы назовём её applyLog. Однако поскольку значение типа (a,String) не несёт с собой контекст возможной неудачи, но несёт контекст добавочного значения журнала, функция applyLog будет обеспечивать сохранность первоначального значения журнала, объединяя его со значением журнала, возвращаемого функцией. Вот реализация этой функции:
applyLog :: (a,String) –> (a –> (b,String)) –> (b,String)
applyLog (x,log) f = let (y,newLog) = f x in (y,log ++ newLog)
Когда у нас есть значение с контекстом и мы хотим передать его функции, то мы обычно пытаемся отделить фактическое значение от контекста, затем пытаемся применить функцию к этому значению, а потом смотрим, сбережён ли контекст. В монаде Maybe мы проверяли, было ли значение равно Just x, и если было, мы брали это значение x и применяли к нему функцию. В данном случае очень просто определить фактическое значение, потому что мы имеем дело с парой, где один компонент является значением, а второй – журналом. Так что сначала мы просто берём значение, то есть x, и применяем к нему функцию f. Мы получаем пару (y,newLog), где y является новым результатом, а newLog – новым журналом. Но если мы вернули это в качестве результата, прежнее значение журнала не было бы включено в результат, так что мы возвращаем пару (y,log ++ newLog). Мы используем операцию конкатенации ++, чтобы добавить новый журнал к прежнему.
Вот функция applyLog в действии:
ghci> (3, "Небольшая банда.") `applyLog` isBigGang
(False,"Небольшая банда.Размер банды сравнён с 9.")
ghci> (30, "Бешеный взвод.") `applyLog` isBigGang
(True,"Бешеный взвод.Размер банды сравнён с 9.")
Результаты аналогичны предшествующим, только теперь количеству бандитов сопутствует журнал, который включён в окончательный журнал.
Вот ещё несколько примеров использования applyLog:
ghci> ("Тобин","Вне закона.") `applyLog` (x –> (length x "Длина."))
(5,"Вне закона.Длина.")
ghci> ("Котопёс","Вне закона.") `applyLog` (x –> (length x "Длина."))
(7,"Вне закона.Длина.")
Смотрите, как внутри анонимной функции образец x является просто нормальной строкой, а не кортежем, и как функция applyLog заботится о добавлении записей журнала.