Изучай Haskell во имя добра! - Миран Липовача
Шрифт:
Интервал:
Закладка:
ПРИМЕЧАНИЕ. Функция return ничем не похожа на оператор return из других языков программирования, таких как C++ или Java. Она не завершает выполнение функции. Она просто принимает обычное значение и помещает его в контекст.
Следующей функцией является >>=, или связывание. Она похожа на применение функции, но вместо того, чтобы получать обычное значение и передавать его обычной функции, она принимает монадическое значение (то есть значение с контекстом) и передаёт его функции, которая принимает обычное значение, но возвращает монадическое.
Затем у нас есть операция >>. Мы пока не будем обращать на неё большого внимания, потому что она идёт в реализации по умолчанию, и её редко реализуют при создании экземпляров класса Monad. Мы подробно рассмотрим её в разделе «Банан на канате».
Последним методом в классе типов Monad является функция fail. Мы никогда не используем её в нашем коде явно. Вместо этого её использует язык Haskell, чтобы сделать возможным неуспешное окончание вычислений в специальной синтаксической конструкции для монад, с которой вы встретитесь позже. Нам не нужно сейчас сильно беспокоиться по поводу этой функции.
Теперь, когда вы знаете, как выглядит класс типов Monad, давайте посмотрим, каким образом для типа Maybe реализован экземпляр этого класса!
instance Monad Maybe where
return x = Just x
Nothing >>= f = Nothing
Just x >>= f = f x
fail _ = Nothing
Функция return аналогична функции pure, так что для работы с ней не нужно большого ума. Мы делаем то же, что мы делали в классе типов Applicative, и оборачиваем в конструктор Just. Операция >>= аналогична нашей функции applyMaybe. Когда мы передаём значение типа Maybe a нашей функции, то запоминаем контекст и возвращаем значение Nothing, если значением слева является Nothing. Ещё раз: если значение отсутствует, нет способа применить к нему функцию. Если это значение Just, мы берём то, что находится внутри, и применяем к этому функцию f.
Мы можем поиграть с типом Maybe как с монадой:
ghci> return "ЧТО" :: Maybe String
Just "ЧТО"
ghci> Just 9 >>= x –> return (x*10)
Just 90
ghci> Nothing >>= x –> return (x*10)
Nothing
В первой строке нет ничего нового или захватывающего, поскольку мы уже использовали функцию pure с типом Maybe, и мы знаем, что функция return – это просто функция pure под другим именем.
Следующая пара строк демонстрирует операцию >>= уже поинтереснее. Обратите внимание: когда мы передавали значение Just 9 анонимной функции x –> return (x*10), то параметр x принимал значение 9 внутри функции. Выглядит это так, будто мы могли извлечь значение из обёртки Maybe без сопоставления с образцом. И мы всё ещё не потеряли контекст нашего значения Maybe, потому что когда оно равно Nothing, результатом использования операции >>= тоже будет Nothing.
Прогулка по канату
Теперь, когда вы знаете, как передавать значение типа Maybe a функции типа a –> Maybe b, учитывая контекст возможной неудачи в вычислениях, давайте посмотрим, как можно многократно использовать операцию >>= для обработки
вычислений нескольких значений Maybe a.
Пьер решил сделать рабочий перерыв на рыбной ферме и попробовать заняться канатоходством. На удивление, ему это неплохо удаётся, но есть одна проблема: на балансировочный шест приземляются птицы! Они прилетают, немного отдыхают, болтают со своими пернатыми друзьями, а затем срываются в поисках хлебных крошек. Это не сильно беспокоило бы Пьера, будь количество птиц c левой стороны шеста всегда равным количеству птиц с правой стороны. Но порой всем птицам почему-то больше нравится одна сторона. В результате канатоходец теряет равновесие и падает (не волнуйтесь, он использует сетку безопасности!).
Давайте предположим, что Пьер удержит равновесие, если количество птиц на левой стороне шеста и на правой стороне шеста разнится в пределах трёх. Покуда, скажем, на правой стороне одна птица, а на левой – четыре, всё в порядке. Но стоит пятой птице опуститься на левую сторону, канатоходец теряет равновесие и кубарем летит вниз.
Мы сымитируем посадку и улёт птиц с шеста и посмотрим, останется ли Пьер на канате после некоторого количества прилётов и улётов птиц. Например, нам нужно увидеть, что произойдёт с Пьером, если первая птица прилетит на левую сторону, затем четыре птицы займут правую, а потом птица, которая была на левой стороне, решит улететь.
Код, код, код
Мы можем представить шест в виде простой пары целых чисел. Первый компонент будет обозначать количество птиц на левой стороне, а второй – количество птиц на правой:
type Birds = Int
type Pole = (Birds, Birds)
Сначала мы создали синоним типа для Int, названный Birds, потому что мы используем целые числа для представления количества имеющихся птиц. Затем создали синоним типа (Birds, Birds) и назвали его Pole (учтите: это означает «шест» – ничего общего ни с поляками, ни с человеком по имени Поль).
А теперь как насчёт того, чтобы добавить функции, которые принимают количество птиц и производят их приземление на одной стороне шеста или на другой?
landLeft :: Birds –> Pole –> Pole
landLeft n (left, right) = (left + n, right)
landRight :: Birds –> Pole –> Pole
landRight n (left, right) = (left, right + n)
Давайте проверим их:
ghci> landLeft 2 (0, 0)
(2,0)
ghci> landRight 1 (1, 2)
(1,3)
ghci> landRight (-1) (1,2)
(1,1)
Чтобы заставить птиц улететь, мы просто произвели приземление отрицательного количества птиц на одной стороне. Поскольку приземление птицы на Pole возвращает Pole, мы можем сцепить применения функций landLeft и landRight:
ghci> landLeft 2 (landRight 1 (landLeft 1 (0, 0)))
(3,1)
Когда мы применяем функцию landLeft 1 к значению (0, 0), у нас получается результат (1, 0). Затем мы усаживаем птицу на правой стороне, что даёт в результате (1, 1). Наконец, две птицы приземляются на левой стороне, что даёт в результате (3, 1). Мы применяем функцию к чему-либо, сначала записывая функцию, а затем её параметр, но здесь было бы лучше, если бы первым шел шест, а потом функция посадки. Предположим, мы создали вот такую функцию:
x -: f = f x
Можно применять функции, сначала записывая параметр, а затем функцию:
ghci> 100 -: (*3)
300
ghci> True -: not
False
ghci> (0, 0) -: landLeft 2
(2,0)
Используя эту форму, мы можем многократно производить приземление птиц на шест в более «читабельном» виде:
ghci> (0, 0) -: landLeft 1 -: landRight 1 -: landLeft 2
(3,1)
Круто!.. Эта версия эквивалентна предыдущей, где мы многократно усаживали птиц на шест, но выглядит она яснее. Здесь очевиднее, что мы начинаем с (0, 0), а затем усаживаем одну птицу слева, потом одну – справа, и в довершение две – слева.
Я улечу
Пока всё идёт нормально, но что произойдёт, если десять птиц приземлятся на одной стороне?
ghci> landLeft 10 (0, 3)
(10,3)
Десять птиц с левой стороны и лишь три с правой?! Этого достаточно, чтобы отправить в полёт самого Пьера!.. Довольно очевидная вещь. Но что если бы у нас была примерно такая последовательность посадок:
ghci> (0, 0) -: landLeft 1 -: landRight 4 -: landLeft (-1) -: landRight (-2)
(0,2)
Может показаться, что всё хорошо, но если вы проследите за шагами, то увидите, что на правой стороне одновременно находятся четыре птицы – а на левой ни одной! Чтобы исправить это, мы должны ещё раз взглянуть на наши функции landLeft и landRight.
Необходимо дать функциям landLeft и landRight возможность завершаться неуспешно. Нам нужно, чтобы они возвращали новый шест, если равновесие поддерживается, но завершались неуспешно, если птицы приземляются неравномерно. И какой способ лучше подойдёт для добавления к значению контекста неудачи, чем использование типа Maybe? Давайте переработаем эти функции:
landLeft :: Birds –> Pole –> Maybe Pole
landLeft n (left,right)
| abs ((left + n) - right) < 4 = Just (left + n, right)
| otherwise = Nothing
landRight :: Birds –> Pole –> Maybe Pole
landRight n (left,right)
| abs (left - (right + n)) < 4 = Just (left, right + n)
| otherwise = Nothing
Вместо того чтобы вернуть значение типа Pole, эти функции теперь возвращают значения типа Maybe Pole. Они по-прежнему принимают количество птиц и прежний шест, как и ранее, но затем проверяют, выведет ли Пьера из равновесия приземление такого количества птиц. Мы используем охранные выражения, чтобы проверить, меньше ли разница в количестве птиц на новом шесте, чем 4. Если меньше, оборачиваем новый шест в конструктор Just и возвращаем это. Если не меньше, возвращаем значение Nothing, сигнализируя о неудаче.