Пока всё нормально. Функция 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
заботится о добавлении записей журнала.
Моноиды приходят на помощь
Убедитесь, что вы на данный момент знаете, что такое моноиды!
Прямо сейчас функция applyLog
принимает значения типа (a,String)
, но есть ли смысл в том, чтобы тип журнала был String
? Он использует операцию ++ для добавления записей журнала – не будет ли это работать и в отношении любого типа списков, не только списка символов? Конечно же, будет! Мы можем пойти дальше и изменить тип этой функции на следующий:
applyLog :: (a,[c]) –> (a –> (b,[c])) –> (b,[c])