Изучай Haskell во имя добра! - Миран Липовача
Шрифт:
Интервал:
Закладка:
Функция return помещает значение в минимальный контекст, который по-прежнему возвращает это значение в качестве своего результата. Это значит, что, например, для типа Maybe она не вносит никакого неуспеха в вычислениях; для списков – не вносит какую-либо дополнительную недетерминированность.
Вот пробный запуск для нескольких монад:
ghci> Just "двигайся дальше" >>= (x –> return x)
Just "двигайся дальше"
ghci> [1,2,3,4] >>= (x –> return x)
[1,2,3,4]
ghci> putStrLn "Вах!" >>= (x –> return x)
Вах!
В этом примере со списком реализация операции >>= выглядит следующим образом:
xs >>= f = concat (map f xs)
Поэтому когда мы передаём список [1,2,3,4] функции return, сначала она отображает [1,2,3,4], что в результате даёт список списков [[1],[2],[3],[4]]. Затем это конкатенируется, и мы получаем наш изначальный список.
Левое тождество и правое тождество являются, по сути, законами, которые описывают, как должна вести себя функция return. Это важная функция для превращения обычных значений в монадические, и было бы нехорошо, если бы монадическое значение, которое она произвела, имело больше, чем необходимый минимальный контекст.
Ассоциативность
Последний монадический закон говорит, что когда у нас есть цепочка применений монадических функций с помощью операции >>=, не должно иметь значения то, как они вложены. В формальной записи выполнение (m >>= f) >>= g – точно то же, что и выполнение m >>= (x –> f x >>= g).
Гм-м, что теперь тут происходит? У нас есть одно монадическое значение, m, и две монадические функции, f и g. Когда мы выполняем выражение (m >>= f) >>= g, то передаём значение m в функцию f, что даёт в результате монадическое значение. Затем мы передаём это новое монадическое значение функции g. В выражении m >>= (x –> f x >>= g) мы берём монадическое значение и передаём его функции, которая передаёт результат применения f x функции g. Нелегко увидеть, почему обе эти записи равны, так что давайте взглянем на пример, который делает это равенство немного более очевидным.
Помните нашего канатоходца Пьера, который пытался удержать равновесие, в то время как птицы приземлялись на его балансировочный шест? Чтобы симулировать приземление птиц на балансировочный шест, мы создали цепочку из нескольких функций, которые могли вызывать неуспешное окончание вычислений:
ghci> return (0, 0) >>= landRight 2 >>= landLeft 2 >>= landRight 2
Just (2,4)
Мы начали со значения Just (0, 0), а затем связали это значение со следующей монадической функцией landRight 2. Результатом было другое монадическое значение, связанное со следующей монадической функцией, и т. д. Если бы надлежало явно заключить это в скобки, мы написали бы следующее:
ghci> ((return (0, 0) >>= landRight 2) >>= landLeft 2) >>= landRight 2
Just (2,4)
Но мы также можем записать инструкцию вот так:
return (0, 0) >>= (x –>
landRight 2 x >>= (y –>
landLeft 2 y >>= (z –>
landRight 2 z)))
Вызов return (0, 0) – то же самое, что Just (0, 0), и когда мы передаём это анонимной функции, образец x принимает значение (0, 0). Функция landRight принимает количество птиц и шест (кортеж, содержащий числа) – и это то, что ей передаётся. В результате мы имеем значение Just (0, 2), и, когда передаём его следующей анонимной функции, образец y становится равен (0, 2). Это продолжается до тех пор, пока последнее приземление птицы не вернёт в качестве результата значение Just (2, 4), что в действительности является результатом всего выражения.
Поэтому неважно, как у вас вложена передача значений монадическим функциям. Важен их смысл. Давайте рассмотрим ещё один способ реализации этого закона. Предположим, мы производим композицию двух функций, f и g:
(.) :: (b –> c) –> (a –> b) –> (a –> c)
f . g = (x –> f (g x))
Если функция g имеет тип a –> b и функция f имеет тип b –> c, мы компонуем их в новую функцию типа a –> c, чтобы её параметр передавался между этими функциями. А что если эти две функции – монадические? Что если возвращаемые ими значения были бы монадическими? Если бы у нас была функция типа a –> m b, мы не могли бы просто передать её результат функции типа b –> m c, потому что эта функция принимает обычное значение b, не монадическое. Чтобы всё-таки достичь нашей цели, можно воспользоваться операцией <=<:
(<=<) :: (Monad m) => (b –> m c) –> (a –> m b) –> (a –> m c)
f <=< g = (x –> g x >>= f)
Поэтому теперь мы можем производить композицию двух монадических функций:
ghci> let f x = [x,-x]
ghci> let g x = [x*3,x*2]
ghci> let h = f <=< g
ghci> h 3
[9,-9,6,-6]
Ладно, всё это здорово. Но какое это имеет отношение к закону ассоциативности? Просто, когда мы рассматриваем этот закон как закон композиций, он утверждает, что f <=< (g <=< h) должно быть равнозначно (f <=< g) <=< h. Это всего лишь ещё один способ доказать, что для монад вложенность операций не должна иметь значения.
Если мы преобразуем первые два закона так, чтобы они использовали операцию <=<, то закон левого тождества утверждает, что для каждой монадической функции f выражение f <=< return означает то же самое, что просто вызвать f. Закон правого тождества говорит, что выражение return <=< f также ничем не отличается от простого вызова f. Это подобно тому, как если бы f являлась обычной функцией, и тогда (f . g) . h было бы аналогично f . (g . h), выражение f . id – всегда аналогично f, и выражение id . f тоже ничем не отличалось бы от вызова f.
В этой главе мы в общих чертах ознакомились с монадами и изучили, как работают монада Maybe и списковая монада. В следующей главе мы рассмотрим целую кучу других крутых монад, а также создадим нашу собственную.
14
Ещё немного монад
Мы видели, как монады могут быть использованы для получения значений с контекстами и применения их к функциям и как использование оператора >>= или нотации do позволяет нам сфокусироваться на самих значениях, в то время как контекст обрабатывается за нас.
Мы познакомились с монадой Maybe и увидели, как она добавляет к значениям контекст возможного неуспеха в вычислениях. Мы узнали о списковой монаде и увидели, как легко она позволяет нам вносить недетерминированность в наши программы. Мы также научились работать в монаде IO даже до того, как вообще выяснили, что такое монада!
В этой главе мы узнаем ещё о нескольких монадах. Мы увидим, как они могут сделать наши программы понятнее, позволяя нам обрабатывать все типы значений как монадические значения. Исследование ряда примеров также укрепит наше понимание монад.
Все монады, которые нам предстоит рассмотреть, являются частью пакета mtl. В языке Haskell пакетом является совокупность модулей. Пакет mtl идёт в поставке с Haskell Platform, так что он у вас, вероятно, уже есть. Чтобы проверить, так ли это, выполните команду ghc-pkg list в командной строке. Эта команда покажет, какие пакеты для языка Haskell у вас уже установлены; одним из таких пакетов должен являться mtl, за названием которого следует номер версии.
Writer? Я о ней почти не знаю!
Итак, мы зарядили наш пистолет монадой Maybe, списковой монадой и монадой IO. Теперь давайте поместим в патронник монаду Writer и посмотрим, что произойдёт, когда мы выстрелим ею!
Между тем как Maybe предназначена для значений с добавленным контекстом неуспешно оканчивающихся вычислений, а список – для недетерминированных вычислений, монада Writer предусмотрена для значений, к которым присоединено другое значение, ведущее себя наподобие журнала. Монада Writer позволяет нам производить вычисления, в то же время обеспечивая слияние всех журнальных значений в одно, которое затем присоединяется к результату.
Например, мы могли бы снабдить наши значения строками, которые объясняют, что происходит, возможно, для отладочных целей. Рассмотрите функцию, которая принимает число бандитов в банде и сообщает нам, является ли эта банда крупной. Это очень простая функция:
isBigGang :: Int –> Bool
isBigGang x = x > 9
Ну а что если теперь вместо возвращения значения True или False мы хотим, чтобы функция также возвращала строку журнала, которая сообщает, что она сделала? Что ж, мы просто создаём эту строку и возвращаем её наряду с нашим значением Bool: