Изучай Haskell во имя добра! - Миран Липовача
Шрифт:
Интервал:
Закладка:
Мы усаживаем птицу слева, а затем проверяем вероятность неудачи и вероятность успеха. В случае неудачи мы возвращаем значение Nothing. В случае успеха усаживаем птиц справа, а затем повторяем всё сызнова. Превращение этого убожества в симпатичную цепочку монадических применений с использованием функции >>= является классическим примером того, как монада Maybe экономит массу времени, когда вам необходимо последовательно выполнить вычисления, основанные на вычислениях, которые могли окончиться неуспешно.
Обратите внимание, каким образом реализация операции >>= для типа Maybe отражает именно эту логику, когда проверяется, равно ли значение Nothing, и действие производится на основе этих сведений. Если значение равно Nothing, она незамедлительно возвращает результат Nothing. Если значение не равно Nothing, она продолжает работу с тем, что находится внутри конструктора Just.
В этом разделе мы рассмотрели, как некоторые функции работают лучше, когда возвращаемые ими значения поддерживают неудачу. Превращая эти значения в значения типа Maybe и заменяя обычное применение функций вызовом операции >>=, мы практически даром получили механизм обработки вычислений, которые могут оканчиваться неудачно. Причина в том, что операция >>= должна сохранять контекст значения, к которому она применяет функции. В данном случае контекстом являлось то, что наши значения были значениями с неуспехом в вычислениях. Поэтому когда мы применяли к таким значениям функции, всегда учитывалась вероятность неуспеха.
Нотация do
Монады в языке Haskell настолько полезны, что они обзавелись своим собственным синтаксисом, который называется «нотация do». Вы уже познакомились с нотацией do в главе 8, когда мы использовали её для объединения нескольких действий ввода-вывода. Как оказывается, нотация do предназначена не только для системы ввода-вывода, но может использоваться для любой монады. Её принцип остаётся прежним: последовательное «склеивание» монадических значений.
Рассмотрим этот знакомый пример монадического применения:
ghci> Just 3 >>= (x –> Just (show x ++ "!"))
Just "3!"
Это мы уже проходили! Передача монадического значения функции, которая возвращает монадическое значение, – ничего особенного. Заметьте, как параметр x становится равным значению 3 внутри анонимной функции, когда мы выполняем код. Как только мы внутри этой анонимной функции, это просто обычное значение, а не монадическое. А что если бы у нас был ещё один вызов оператора >>= внутри этой функции? Посмотрите:
ghci> Just 3 >>= (x –> Just "!" >>= (y –> Just (show x ++ y)))
Just "3!"
Ага-а, вложенное использование операции >>=! Во внешней анонимной функции мы передаём значение Just "!" анонимной функции y –> Just (show x ++ y). Внутри этой анонимной функции параметр y становится равным "!". Параметр x по-прежнему равен 3, потому что мы получили его из внешней анонимной функции. Всё это как будто напоминает мне о следующем выражении:
ghci> let x = 3; y = "!" in show x ++ y
"3!"
Главное отличие состоит в том, что значения в нашем примере с использованием оператора >>= являются монадическими. Это значения с контекстом неудачи. Мы можем заменить любое из них на неудачу:
ghci> Nothing >>= (x –> Just "!" >>= (y –> Just (show x ++ y))) Nothing
ghci> Just 3 >>= (x –> Nothing >>= (y –> Just (show x ++ y)))
Nothing
ghci> Just 3 >>= (x –> Just "!" >>= (y –> Nothing))
Nothing
В первой строке передача значения Nothing функции естественным образом даёт в результате Nothing. Во второй строке мы передаём значение Just 3 функции, и параметр x становится равным 3. Но потом мы передаём значение Nothing внутренней анонимной функции, и результатом становится Nothing, что заставляет внешнюю анонимную функцию тоже произвести Nothing в качестве своего результата. Это что-то вроде присвоения значений переменным в выражениях let, только значения, о которых идёт речь, являются монадическими.
Чтобы проиллюстрировать эту идею, давайте запишем следующие строки в сценарий так, чтобы каждое значение типа Maybe занимало свою собственную строку:
foo :: Maybe String
foo = Just 3 >>= (x –>
Just "!">>= (y –>
Just (show x ++ y)))
Чтобы уберечь нас от написания всех этих раздражающих анонимных функций, язык Haskell предоставляет нам нотацию do. Она позволяет нам записать предыдущий кусок кода вот так:
foo :: Maybe String
foo = do
x <– Just 3
y <– Just "!"
Just (show x ++ y)
Могло показаться, что мы получили возможность временно извлекать сущности из значений типа Maybe без необходимости проверять на каждом шагу, являются ли значения типа Maybe значениями в конструкторе Just или значениями Nothing. Вот классно!.. Если какое-либо из значений, которые мы пытаемся извлечь, равно Nothing, всё выражение do в результате вернёт значение Nothing. Мы выдёргиваем наружу их значения (если таковые существуют) и перекладываем необходимость беспокойства о контексте, идущем с этими значениями, на плечи оператора >>=.
Выражения do – это просто другой синтаксис для сцепления монадических значений.
Делай как я
В выражении do каждая строка, не являющаяся строкой let, является монадическим значением. Чтобы просмотреть её результат, мы используем символ <–. Если у нас есть значение типа Maybe String и мы привязываем её к образцу с помощью символа <–, этот образец будет иметь тип String так же, как когда мы использовали операцию >>= для передачи монадических значений анонимным функциям.
Последнее монадическое значение в выражении do – такое как Just (show x ++ y) в этом примере – не может быть использовано с символом <– для привязки его результата, потому что если бы мы преобразовали выражение do обратно в цепочку применений оператора >>=, это не имело бы смысла. Наоборот, результат последнего выражения является результатом всего склеенного монадического значения, учитывая возможную неудачу вычисления каждого из предыдущих монадических значений. Рассмотрите, например, следующую строку:
ghci> Just 9 >>= (x –> Just (x > 8))
Just True
Поскольку левым параметром функции >>= является значение в конструкторе Just, анонимная функция применяется к значению 9, и результатом становится значение Just True. Мы можем переписать это в нотации do следующим образом:
marySue :: Maybe Bool
marySue = do
x <– Just 9
Just (x > 8)
Сравнивая оба варианта, легко увидеть, почему результатом всего монадического значения является результат последнего монадического значения в выражении do со всеми предыдущими монадическими значениями, сцепленными с ним.
Пьер возвращается
Инструкция нашего канатоходца может также быть выражена с использованием нотации do. Функции landLeft и landRight принимают количество птиц и шест и производят шест, обёрнутый в Just. Исключение – это когда канатоходец соскальзывает, и тогда возвращается значение Nothing. Мы использовали операцию >>= для сцепления последовательных шагов, потому что каждый из них зависел от предыдущего и каждый обладал добавленным контекстом возможной неудачи. Здесь две птицы приземляются с левой стороны, затем две птицы – с правой, а потом одна птица – снова с левой:
routine :: Maybe Pole
routine = do
start <– return (0, 0)
first <– landLeft 2 start
second <– landRight 2 first
landLeft 1 second
Давайте посмотрим, окончится ли это удачно для Пьера:
ghci> routine
Just (3,2)
Окончилось удачно!
Когда мы выполняли эти инструкции, явно записывая вызовы оператора >>=, мы обычно писали что-то вроде return (0, 0) >>= landLeft 2, потому что функция landLeft является функцией, которая возвращает значение типа Maybe. Однако при использовании выражения do каждая строка должна представлять монадическое значение. Поэтому мы явно передаём предыдущее значение типа Pole функциям landLeft и landRight. Если бы мы проверили образцы, к которым привязали наши значения типа Maybe, то start был бы равен (0, 0), first был бы равен (2, 0) и т. д.