Моноиды приходят на помощь

Убедитесь, что вы на данный момент знаете, что такое моноиды!

Прямо сейчас функция applyLog принимает значения типа (a,String), но есть ли смысл в том, чтобы тип журнала был String? Он использует операцию ++ для добавления записей журнала – не будет ли это работать и в отношении любого типа списков, не только списка символов? Конечно же, будет! Мы можем пойти дальше и изменить тип этой функции на следующий:

applyLog :: (a,[c]) –> (a –> (b,[c])) –> (b,[c])

Теперь журнал является списком. Тип значений, содержащихся в списке, должен быть одинаковым как для изначального списка, так и для списка, который возвращает функция; в противном случае мы не смогли бы использовать операцию ++ для «склеивания» их друг с другом.

Сработало бы это для строк байтов? Нет причины, по которой это не сработало бы! Однако тип, который у нас имеется, работает только со списками. Похоже, что нам пришлось бы создать ещё одну функцию applyLog для строк байтов. Но подождите! И списки, и строки байтов являются моноидами. По существу, те и другие являются экземплярами класса типов Monoid, а это значит, что они реализуют функцию mappend. Как для списков, так и для строк байтов функция mappend производит конкатенацию. Смотрите:

ghci> [1,2,3] `mappend` [4,5,6]

[1,2,3,4,5,6]

ghci> B.pack [99,104,105] `mappend` B.pack [104,117,97,104,117,97]

Chunk "chi" (Chunk "huahua" Empty)

Круто! Теперь наша функция applyLog может работать для любого моноида. Мы должны изменить тип, чтобы отразить это, а также реализацию, потому что следует заменить вызов операции ++ вызовом функции mappend:

applyLog :: (Monoid m) => (a,m) –> (a –> (b,m)) –> (b,m)

applyLog (x,log) f = let (y,newLog) = f x

                     in (y,log `mappend` newLog)

Поскольку сопутствующее значение теперь может быть любым моноидным значением, нам больше не нужно думать о кортеже как о значении и журнале, но мы можем думать о нём как о значении с сопутствующим моноидным значением. Например, у нас может быть кортеж, в котором есть имя предмета и цена предмета в виде моноидного значения. Мы просто используем определение типа newtype Sum, чтобы быть уверенными, что цены добавляются, пока мы работаем с предметами. Вот функция, которая добавляет напиток к обеду какого-то ковбоя:

import Data.Monoid

type Food = String

type Price = Sum Int

addDrink :: Food –> (Food,Price)

addDrink "бобы" = ("молоко", Sum 25)

addDrink "вяленое мясо" = ("виски", Sum 99)

addDrink _ = ("пиво", Sum 30)

Мы используем строки для представления продуктов и тип Int в обёртке типа newtype Sum для отслеживания того, сколько центов стоит тот или иной продукт. Просто напомню: выполнение функции mappend для значений типа Sum возвращает сумму обёрнутых значений.

ghci> Sum 3 `mappend` Sum 9

Sum {getSum = 12}

Функция addDrink довольно проста. Если мы едим бобы, она возвращает "молоко" вместе с Sum 25; таким образом, 25 центов завёрнуты в конструктор Sum. Если мы едим вяленое мясо, то пьём виски, а если едим что-то другое – пьём пиво. Обычное применение этой функции к продукту сейчас было бы не слишком интересно, а вот использование функции applyLog для передачи продукта с указанием цены в саму функцию представляет интерес:

ghci> ("бобы", Sum 10) `applyLog` addDrink

("молоко",Sum {getSum = 35})

ghci> ("вяленое мясо", Sum 25) `applyLog` addDrink

("виски",Sum {getSum = 124})

ghci> ("собачатина", Sum 5) `applyLog` addDrink

("пиво",Sum {getSum = 35})

Молоко стоит 25 центов, но если мы заедаем его бобами за 10 центов, это обходится нам в 35 центов. Теперь ясно, почему присоединённое значение не всегда должно быть журналом – оно может быть любым моноидным значением, и то, как эти два значения объединяются, зависит от моноида. Когда мы производили записи в журнал, они присоединялись в конец, но теперь происходит сложение чисел.

Поскольку значение, возвращаемое функцией addDrink, является кортежем типа (Food,Price), мы можем передать этот результат функции addDrink ещё раз, чтобы функция сообщила нам, какой напиток будет подан в сопровождение к блюду и сколько это нам будет стоить. Давайте попробуем:

ghci> ("собачатина", Sum 5) `applyLog` addDrink `applyLog` addDrink

("пиво",Sum {getSum = 65})

Добавление напитка к какой-нибудь там собачатине вернёт пиво и дополнительные 30 центов, то есть ("пиво", Sum 35). А если мы используем функцию applyLog для передачи этого результата функции addDrink, то получим ещё одно пиво, и результатом будет ("пиво", Sum 65).