Изучай Haskell во имя добра! - Миран Липовача
Шрифт:
Интервал:
Закладка:
Поскольку значение, возвращаемое функцией addDrink, является кортежем типа (Food,Price), мы можем передать этот результат функции addDrink ещё раз, чтобы функция сообщила нам, какой напиток будет подан в сопровождение к блюду и сколько это нам будет стоить. Давайте попробуем:
ghci> ("собачатина", Sum 5) `applyLog` addDrink `applyLog` addDrink
("пиво",Sum {getSum = 65})
Добавление напитка к какой-нибудь там собачатине вернёт пиво и дополнительные 30 центов, то есть ("пиво", Sum 35). А если мы используем функцию applyLog для передачи этого результата функции addDrink, то получим ещё одно пиво, и результатом будет ("пиво", Sum 65).
Тип Writer
Теперь, когда мы увидели, что значение с присоединённым моноидом ведёт себя как монадическое значение, давайте исследуем экземпляр класса 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
logNumber x = Writer (x, ["Получено число: " ++ show x])
multWithLog :: Writer [String] Int
multWithLog = do
a <– logNumber 3
b <– logNumber 5
return (a*b)
Функция logNumber принимает число и создаёт из него значение типа Writer. Для моноида мы используем список строк и снабжаем число одноэлементным списком, который просто говорит, что мы получили это число. Функция multWithLog – это значение типа Writer, которое перемножает 3 и 5 и гарантирует включение прикреплённых к ним журналов в окончательный журнал. Мы используем функцию return, чтобы вернуть значение (a*b) в качестве результата. Поскольку функция return просто берёт что-то и помещает в минимальный контекст, мы можем быть уверены, что она ничего не добавит в журнал. Вот что мы увидим, если выполним этот код:
ghci> runWriter multWithLog
(15,["Получено число: 3","Получено число: 5"])
Добавление в программы функции журналирования
Иногда мы просто хотим, чтобы некое моноидное значение было включено в каком-то определённом месте. Для этого может пригодиться функция tell. Она является частью класса типов MonadWriter и в случае с типом Writer берёт монадическое значение вроде ["Всё продолжается"] и создаёт значение типа Writer, которое возвращает значение-пустышку () в качестве своего результата, но прикрепляет желаемое моноидное значение. Когда у нас есть монадическое значение, которое в качестве результата содержит значение (), мы не привязываем его к переменной. Вот определение функции multWithLog с включением некоторых дополнительных сообщений:
multWithLog :: Writer [String] Int
multWithLog = do
a <– logNumber 3
b <– logNumber 5
tell ["Перемножим эту парочку"]
return (a*b)
Важно, что вызов return (a*b) находится в последней строке, потому что результат последней строки в выражении do является результатом всего выражения do. Если бы мы поместили вызов функции tell на последнюю строку, результатом этого выражения do было бы (). Мы бы потеряли результат умножения. Однако журнал остался бы прежним. Вот функция в действии:
ghci> runWriter multWithLog
(15,["Получено число: 3","Получено число: 5","Перемножим эту парочку"])
Добавление журналирования в программы
Алгоритм Евклида – это алгоритм, который берёт два числа и вычисляет их наибольший общий делитель, то есть самое большое число, на которое делятся без остатка оба числа. В языке Haskell уже имеется функция gcd, которая проделывает это, но давайте реализуем её сами, а затем снабдим её возможностями журналирования. Вот обычный алгоритм:
gcd' :: Int –> Int –> Int
gcd' a b
| b == 0 = a
| otherwise = gcd' b (a `mod` b)
Алгоритм очень прост. Сначала он проверяет, равно ли второе число 0. Если равно, то результатом становится первое число. Если не равно, то результатом становится наибольший общий делитель второго числа и остаток от деления первого числа на второе. Например, если мы хотим узнать, каков наибольший общий делитель 8 и 3, мы просто следуем изложенному алгоритму. Поскольку 3 не равно 0, мы должны найти наибольший общий делитель 3 и 2 (если мы разделим 8 на 3, остатком будет 2). Затем ищем наибольший общий делитель 3 и 2. Число 2 по-прежнему не равно 0, поэтому теперь у нас есть 2 и 1. Второе число не равно 0, и мы выполняем алгоритм ещё раз для 1 и 0, поскольку деление 2 на 1 даёт нам остаток равный 0. И наконец, поскольку второе число равно 0, финальным результатом становится 1. Давайте посмотрим, согласуется ли наш код: