Я улечу
Пока всё идёт нормально, но что произойдёт, если десять птиц приземлятся на одной стороне?
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, сигнализируя о неудаче.
Давайте опробуем этих деток:
ghci> landLeft 2 (0, 0)
Just (2,0)
ghci> landLeft 10 (0, 3)
Nothing
Когда мы приземляем птиц, не выводя Пьера из равновесия, мы получаем новый шест, обёрнутый в конструктор Just. Но когда значительное количество птиц в итоге оказывается на одной стороне шеста, в результате мы получаем значение Nothing. Всё это здорово, но, похоже, мы потеряли возможность многократного приземления птиц на шесте! Выполнить landLeft 1 (landRight 1 (0, 0)) больше нельзя, потому что когда landRight 1 применяется к (0, 0), мы получаем значение не типа Pole, а типа Maybe Pole. Функция landLeft 1 принимает параметр типа Pole, а не Maybe Pole.
Нам нужен способ получения Maybe Pole и передачи его функции, которая принимает Pole и возвращает Maybe Pole. К счастью, у нас есть операция >>=, которая делает именно это для типа Maybe. Давайте попробуем:
ghci> landRight 1 (0, 0) >>= landLeft 2
Just (2,1)
Вспомните, что функция landLeft 2 имеет тип Pole –> Maybe Pole. Мы не можем просто передать ей значение типа Maybe Pole, которое является результатом вызова функции landRight 1 (0, 0), поэтому используем операцию >>=, чтобы взять это значение с контекстом и отдать его функции landLeft 2. Операция >>= действительно позволяет нам обрабатывать значения типа Maybe как значения с контекстом. Если мы передадим значение Nothing в функцию landLeft 2, результатом будет Nothing, и неудача будет распространена:
ghci> Nothing >>= landLeft 2
Nothing
Используя это, мы теперь можем помещать в цепочку приземления, которые могут окончиться неуспешно, потому что оператор >>= позволяет нам передавать монадическое значение функции, которая принимает обычное значение. Вот последовательность приземлений птиц:
ghci> return (0, 0) >>= landRight 2 >>= landLeft 2 >>= landRight 2
Just (2,4)
Вначале мы использовали функцию return, чтобы взять шест и обернуть его в конструктор Just. Мы могли бы просто применить выражение landRight 2 к значению (0, 0) – это было бы то же самое, – но так можно добиться большего единообразия, используя оператор >>= для каждой функции. Выражение Just (0, 0) передаётся в функцию landRight 2, что в результате даёт результат Just (0, 2). Это значение в свою очередь передаётся в функцию landLeft 2, что в результате даёт новый результат (2, 2), и т. д.
Помните следующий пример, прежде чем мы ввели возможность неудачи в инструкции Пьера?
ghci> (0, 0) -: landLeft 1 -: landRight 4 -: landLeft (-1) -: landRight (-2)
(0,2)
Он не очень хорошо симулировал взаимодействие канатоходца с птицами. В середине его равновесие было нарушено, но результат этого не отразил. Давайте теперь исправим это, используя монадическое применение (оператор >>=) вместо обычного:
ghci> return (0, 0) >>= landLeft 1 >>= landRight 4 >>= landLeft (-1) >>= landRight (-2)
Nothing
Окончательный результат представляет неудачу, чего мы и ожидали. Давайте посмотрим, как этот результат был получен:
1. Функция return помещает значение (0, 0) в контекст по умолчанию, превращая значение в Just (0, 0).
2. Происходит вызов выражения Just (0, 0) >>= landLeft 1. Поскольку значение Just (0, 0) является значением Just, функция landLeft 1 применяется к (0, 0), что в результате даёт результат Just (1, 0), потому что птицы всё ещё находятся в относительном равновесии.
3. Имеет место вызов выражения Just (1, 0) >>= landRight 4, и результатом является выражение Just (1, 4), поскольку равновесие птиц пока ещё не затронуто, хотя Пьер уже удерживается с трудом.
4. Выражение Just (1, 4) передаётся в функцию landLeft (–1). Это означает, что имеет место вызов landLeft (–1) (1, 4). Теперь ввиду особенностей работы функции landLeft в результате это даёт значение Nothing, так как результирующий шест вышел из равновесия.
5. Теперь, поскольку у нас есть значение Nothing, оно передаётся в функцию landRight (–2), но так как это Nothing, результатом автоматически становится Nothing, поскольку нам не к чему применить эту функцию.
Мы не смогли бы достигнуть этого, просто используя Maybe в качестве аппликативного функтора. Если вы попробуете так сделать, то застрянете, поскольку аппликативные функторы не очень-то позволяют аппликативным значениям взаимодействовать друг с другом. Их в лучшем случае можно использовать как параметры для функции, применяя аппликативный стиль.
Аппликативные операторы извлекут свои результаты и передадут их функции в соответствующем для каждого аппликативного функтора виде, а затем соберут окончательное аппликативное значение, но взаимодействие между ними не особенно заметно. Здесь, однако, каждый шаг зависит от результата предыдущего шага. Во время каждого приземления возможный результат предыдущего шага исследуется, а шест проверяется на равновесие. Это определяет, окончится ли посадка успешно либо неуспешно.