Теперь, когда мы увидели, что значение с присоединённым моноидом ведёт себя как монадическое значение, давайте исследуем экземпляр класса Monad
для типов таких значений. Модуль Control.Monad.Writer
экспортирует тип Writer w a
со своим экземпляром класса Monad
и некоторые полезные функции для работы со значениями такого типа.
Прежде всего, давайте исследуем сам тип. Для присоединения моноида к значению нам достаточно поместить их в один кортеж. Тип Writer w a
является просто обёрткой newtype
для кортежа. Его определение несложно:
newtype Writer w a = Writer { runWriter :: (a, w) }
Чтобы кортеж мог быть сделан экземпляром класса Monad
и его тип был отделён от обычного кортежа, он обёрнут в newtype
. Параметр типа a
представляет тип значения, параметр типа w
– тип присоединённого значения моноида.
Экземпляр класса Monad
для этого типа определён следующим образом:
instance (Monoid w) => Monad (Writer w) where
return x = Writer (x, mempty)
(Writer (x,v)) >>= f = let (Writer (y, v')) = f x
in Writer (y, v `mappend` v')
Во-первых, давайте рассмотрим операцию >>=
. Её реализация по существу аналогична функции applyLog
, только теперь, поскольку наш кортеж обёрнут в тип newtype Writer
, мы должны развернуть его перед сопоставлением с образцом. Мы берём значение x
и применяем к нему функцию f
. Это даёт нам новое значение Writer
w
a
, и мы используем выражение let
для сопоставления его с образцом. Представляем y
в качестве нового результата и используем функцию mappend
для объединения старого моноидного значения с новым. Упаковываем его вместе с результирующим значением в кортеж, а затем оборачиваем с помощью конструктора Writer
, чтобы нашим результатом было значение Writer
, а не просто необёрнутый кортеж.
Ладно, а что у нас с функцией return
? Она должна принимать значение и помещать его в минимальный контекст, который по-прежнему возвращает это значение в качестве результата. Так каким был бы контекст для значений типа Writer
? Если мы хотим, чтобы сопутствующее моноидное значение оказывало на другие моноидные значения наименьшее влияние, имеет смысл использовать функцию mempty
. Функция mempty
используется для представления «единичных» моноидных значений, как, например, ""
, Sum
0
и пустые строки байтов. Когда мы выполняем вызов функции mappend
между значением mempty
и каким-либо другим моноидным значением, результатом будет это второе моноидное значение. Так что если мы используем функцию return
для создания значения монады Writer
, а затем применяем оператор >>=
для передачи этого значения функции, окончательным моноидным значением будет только то, что возвращает функция. Давайте используем функцию return
с числом 3
несколько раз, только каждый раз будем соединять его попарно с другим моноидом:
ghci> runWriter (return 3 :: Writer String Int)
(3,"")
ghci> runWriter (return 3 :: Writer (Sum Int) Int)
(3,Sum {getSum = 0})
ghci> runWriter (return 3 :: Writer (Product Int) Int)
(3,Product {getProduct = 1})
Поскольку у типа Writer
нет экземпляра класса Show
, нам пришлось использовать функцию runWriter
для преобразования наших значений типа Writer
в нормальные кортежи, которые могут быть показаны в виде строки. Для строк единичным значением является пустая строка. Для типа Sum
это значение 0
, потому что если мы прибавляем к чему-то 0
, это что-то не изменяется. Для типа Product
единичным значением является 1
.
В экземпляре класса Monad
для типа Writer
не имеется реализация для функции fail
; значит, если сопоставление с образцом в нотации do
оканчивается неудачно, вызывается функция error
.
Использование нотации do с типом Writer
Теперь, когда у нас есть экземпляр класса Monad
, мы свободно можем использовать нотацию do
для значений типа Writer
. Это удобно, когда у нас есть несколько значений типа Writer
и мы хотим с ними что-либо делать. Как и в случае с другими монадами, можно обрабатывать их как нормальные значения, и контекст сохраняется для нас. В этом случае все моноидные значения, которые идут в присоединённом виде, объединяются с помощью функции mappend
, а потому отражаются в окончательном результате. Вот простой пример использования нотации do
с типом Writer
для умножения двух чисел:
import Control.Monad.Writer
logNumber :: Int –> Writer [String] Int