Совершенствуем наши аппликативные функторы 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Совершенствуем наши аппликативные функторы



Когда мы начали с функторов, вы видели, что можно отоб- ражать разные типы данных с помощью функций, используя класс типов Functor. Введение в функторы заставило нас за- даться вопросом: «Когда у нас есть функция типа a –> b и неко-

торый тип данных f a, как отобразить этот тип данных с помощью функции, чтобы получить значение типа f b?» Вы видели, как с по-


 

 
 

мощью чего-либо отобразить Maybe a, список [a], IO a и т. д. Вы даже видели, как с помощью функции типа a –> b отобразить другие фун- кции типа r –> a, чтобы получить функции типа r –> b. Чтобы отве- тить на вопрос о том, как отобразить некий тип данных с помощью функции, нам достаточно было взглянуть на тип функции fmap:

fmap:: (Functor f) => (a –> b) –> f a –> f b

А затем нам необходимо было просто заставить его работать с нашим типом данных, написав соответствующий экземпляр клас- са Functor.

 
 

Потом вы узнали, что возможно усовершенствование функто- ров, и у вас возникло ещё несколько вопросов. Что если эта фун- кция типа a –> b уже обёрнута в значение функтора? Скажем, у нас есть Just (*3) – как применить это к значению Just 5? Или, может быть, не к Just 5, а к значению Nothing? Или, если у нас есть список [(*2),(+4)], как применить его к списку [1,2,3]? Как это вообще мо- жет работать?.. Для этого был введён класс типов Applicative:

(<*>):: (Applicative f) => f (a –> b) –> f a –> f b

Вы также видели, что можно взять обычное значение и обер- нуть его в тип данных. Например, мы можем взять значение 1 и обернуть его так, чтобы оно превратилось в Just 1. Или можем превратить его в [1]. Оно могло бы даже стать действием ввода-вы- вода, которое ничего не делает, а просто выдаёт 1. Функция, кото- рая за это отвечает, называется pure.

 
 

Аппликативное значение можно рассматривать как значение с добавленным контекстом – «причудливое» значение, выражаясь техническим языком. Например, буква 'a' – это просто обычная буква, тогда как значение Just 'a' обладает неким добавленным контекстом. Вместо типа Char у нас есть тип Maybe Char, который сообщает нам, что его значением может быть буква; но значени- ем может также быть и отсутствие буквы. Класс типов Applicative позволяет нам использовать с этими значениями, имеющими кон- текст, обычные функции, и этот контекст сохраняется. Взгляните на пример:

ghci> (*) <$> Just 2 <*> Just 8 Just 16

ghci> (++) <$> Just "клингон" <*> Nothing Nothing


 

ghci> (-) <$> [3,4] <*> [1,2,3]

 
 

[2,1,0,3,2,1]

Поэтому теперь, когда мы рассматриваем их как аппликатив- ные значения, значения типа Maybe a представляют вычисления, которые могли окончиться неуспешно, значения типа [a] – вычис- ления, которые содержат несколько результатов (недетерминиро- ванные вычисления), значения типа IO a – вычисления, которые имеют побочные эффекты, и т. д.

 
 

Монады являются естественным продолжением аппликатив- ных функторов и предоставляют решение для следующей пробле- мы: если у нас есть значение с контекстом типа m a, как нам при- менить к нему функцию, которая принимает обычное значение a и возвращает значение с контекстом? Другими словами, как нам применить функцию типа a –> m b к значению типа m a? По существу, нам нужна вот эта функция:

(>>=):: (Monad m) => m a –> (a –> m b) –> m b

Если у нас есть причудливое значение и функция, которая при- нимает обычное значение, но возвращает причудливое, как нам пе- редать это причудливое значение в данную функцию? Это является основной задачей при работе с монадами. Мы пишем m a вместо f a, потому что m означает Monad; но монады являются всего лишь аппли- кативными функторами, которые поддерживают операцию >>=. Функция >>= называется связыванием.

Когда у нас есть обычное значение типа a и обычная функция типа a –> b, передать значение функции легче лёгкого: мы при- меняем функцию к значению как обычно – вот и всё! Но когда мы имеем дело со значениями, находящимися в определённом контексте, нужно немного поразмыслить, чтобы понять, как эти причудливые значения передаются функциям и как учесть их по- ведение. Впрочем, вы сами убедитесь, что это так же просто, как раз, два, три.

 

Приступаем к типу Maybe

Теперь, когда у вас появилось хотя бы смутное представление о том, что такое монады, давайте внесём в это представление несколько большую определённость. К великому удивлению, тип Maybe являет-


 

ся монадой. Здесь мы исследуем её чуть лучше, чтобы понять, как она работает в этой роли.

ПРИМЕЧАНИЕ. Убедитесь, что вы в настоящий момент понима- ете, что такое аппликативные функторы (мы обсуждали их в главе 11). Вы должны хорошо разбираться в том, как работают различ- ные экземпляры класса Applicative и какие виды вычислений они представляют. Для понимания монад вам понадобится развить уже имеющиеся знания об аппликативных функторах.

Значение типа Maybe a пред- ставляет значение типа a, но с при- креплённым контекстом возможной неудачи в вычислениях. Значение Just "дхарма" означает, что в нём имеется строка "дхарма". Значение Nothing представляет отсутствие значения, или, если вы посмотрите на строку как на результат вычисле- ния, это говорит о том, что вычис- ление завершилось неуспешно.

Когда мы рассматривали тип Maybe как функтор, мы видели, что если нам нужно отобразить его с по- мощью функции, используя метод

 
 

fmap, функция отображала содержимое, если это значение Just. В противном случае сохранялось значение Nothing, поскольку с помо- щью функции нечего отображать!

ghci> fmap (++"!") (Just "мудрость") Just "мудрость!"

 
 

ghci> fmap (++"!") Nothing Nothing

Тип Maybe функционирует в качестве аппликативного функто- ра аналогично. Однако при использовании аппликативных функ- торов сама функция находится в контексте наряду со значением, к которому она применяется. Тип Maybe является аппликативным функтором таким образом, что когда мы используем операцию <*> для применения функции внутри типа Maybe к значению, которое находится внутри типа Maybe, они оба должны быть значениями


 

 
 

Just, чтобы результатом было значение Just; в противном случае результатом будет значение Nothing. Это имеет смысл. Если недо- стаёт функции либо значения, к которому вы её применяете, вы не можете ничего получить «из воздуха», поэтому вы должны распро- странить неудачу.

ghci> Just (+3) <*> Just 3

Just 6

ghci> Nothing <*> Just "алчность"

Nothing

 
 

ghci> Justord <*> Nothing Nothing

Использование аппликативного стиля, чтобы обычные функ- ции работали со значениями типа Maybe, действует аналогичным образом. Все значения должны быть значениями Just; в противном случае всё это напрасно (Nothing)!

ghci> max <$> Just 3 <*> Just 6 Just 6

 
 

ghci> max <$> Just 3 <*> Nothing Nothing

А теперь давайте подумаем над тем, как бы мы использовали операцию >>= с типом Maybe. Операция >>= принимает монадичес- кое значение и функцию, которая принимает обычное значение. Она возвращает монадическое значение и умудряется применить эту функцию к монадическому значению. Как она это делает, если функция принимает обычное значение? Ну, она должна принимать во внимание контекст этого монадического значения.

 
 

В данном случае операция >>= принимала бы значение типа Maybe a и функцию типа a –> Maybe b и каким-то образом применяла бы эту функцию к значению Maybe a. Чтобы понять, как она это дела- ет, мы будем исходить из того, что тип Maybe является аппликатив- ным функтором. Скажем, у нас есть анонимная функция \x –> Just (x+1). Она принимает число, прибавляет к нему 1 и оборачивает его в конструктор Just:

ghci> (\x –> Just (x+1)) 1 Just 2

 
 

ghci> (\x –> Just (x+1)) 100 Just 101


 

Если мы передадим ей значение 1, она вернёт результат Just 2. Если мы дадим ей значение 100, результатом будет Just 101. Это вы- глядит очень просто. Но как нам передать этой функции значение типа Maybe? Если мы подумаем о том, как тип Maybe работает в ка- честве аппликативного функтора, ответить на этот вопрос будет довольно легко. Мы передаём функции значение Just, берём то, что находится внутри конструктора Just, и применяем к этому фун- кцию. Если мы даём ей значение Nothing, то у нас остаётся функция, но к ней нечего (Nothing) применить. В этом случае давайте сдела- ем то же, что мы делали и прежде, и скажем, что результат равен Nothing.

 
 

Вместо того чтобы назвать функцию >>=, давайте пока назовём её applyMaybe. Она принимает значение типа Maybe a и функцию, ко- торая возвращает значение типа Maybe b, и умудряется применить эту функцию к значению типа Maybe a. Вот она в исходном коде:

applyMaybe:: Maybe a –> (a –> Maybe b) –> Maybe b applyMaybe Nothing f = Nothing

 
 

applyMaybe (Just x) f = f x

Теперь давайте с ней поиграем. Мы будем использовать её как инфиксную функцию так, чтобы значение типа Maybe было слева, а функция была справа:

ghci> Just 3 `applyMaybe` \x –> Just (x+1)

Just 4

ghci> Just "смайлик" `applyMaybe` \x –> Just (x ++ ":)") Just "смайлик:)"

ghci> Nothing `applyMaybe` \x –> Just (x+1)

Nothing

 
 

ghci> Nothing `applyMaybe` \x –> Just (x ++ ":)") Nothing

В данном примере, когда мы использовали функцию applyMaybe со значением Just и функцией, функция просто применялась к зна- чению внутри конструктора Just. Когда мы попытались исполь- зовать её со значением Nothing, весь результат был равен Nothing. Что насчёт того, если функция возвращает Nothing? Давайте по- смотрим:

ghci>Just 3 `applyMaybe` \x –> if x > 2 then Just x else Nothing Just 3


 

 
 

ghci> Just 1 `applyMaybe` \x –> if x > 2 then Just x else Nothing Nothing

Результаты оказались такими, каких мы и ждали! Если монади- ческое значение слева равно Nothing, то всё будет равно Nothing. А если функция справа возвращает значение Nothing, результатом опять будет Nothing. Это очень похоже на тот случай, когда мы ис- пользовали тип Maybe в качестве аппликативного функтора и в ре- зультате получали значение Nothing, если где-то в составе присутс- твовало значение Nothing.

Похоже, мы догадались, как взять причудливое значение, пе- редать его функции, которая принимает обычное значение, и вер- нуть причудливое значение. Мы сделали это, помня, что значение типа Maybe представляет вычисление, которое могло окончиться неуспешно.

Вы можете спросить себя: «Чем это полезно?» Может показать- ся, что аппликативные функторы сильнее монад, поскольку ап- пликативные функторы позволяют нам взять обычную функцию и заставить её работать со значениями, имеющими контекст. В этой главе вы увидите, что монады, будучи усовершенствованными ап- пликативными функторами, тоже способны на такое. На самом деле они могут делать и кое-какие другие крутые вещи, на которые не способны аппликативные функторы.

Мы вернёмся к Maybe через минуту, но сначала давайте взглянем на класс типов, который относится к монадам.

 

Класс типов Monad

 
 

Как и функторы, у которых есть класс типов Functor, и аппликатив- ные функторы, у которых есть класс типов Applicative, монады об- ладают своим классом типов: Monad! (Ух ты, кто бы мог подумать?)

class Monad m where return:: a –> m a

(>>=):: m a –> (a –> m b) –> m b (>>):: m a –> m b –> m b

x >> y = x >>= \_ –> y

 
 

fail:: String –> m a fail msg = error msg


 

В первой строке говорится class Monad m where. Стойте, не гово- рил ли я, что монады являются просто расширенными аппликатив- ными функторами? Не надлежит ли здесь быть ограничению класса наподобие class (Applicative m) => Monad m where, чтобы тип должен был являться аппликативным

функтором, прежде чем он мо- жет быть сделан монадой? Лад- но, положим, надлежит, – но когда появился язык Haskell, людям не пришло в голову, что аппликативные функторы хорошо подходят для этого языка. Тем не менее будьте уве- рены: каждая монада является аппликативным функтором, да- же если в объявлении класса Monad этого не говорится.

Первой функцией, которая объявлена в классе типов Monad, является return. Она аналогич- на функции pure, находящейся в классе типов Applicative. Так

что, хоть она и называется по-другому, вы уже фактически с ней зна- комы. Функция return имеет тип (Monad m) => a –> m a. Она принимает значение и помещает его в минимальный контекст по умолчанию, который по-прежнему содержит это значение. Другими словами, она принимает нечто и оборачивает это в монаду. Мы уже исполь- зовали функцию return при обработке действий ввода-вывода (см. главу 8). Там она понадобилась для получения значения и создания фальшивого действия ввода-вывода, которое ничего не делает, а только возвращает это значение. В случае с типом Maybe она при- нимает значение и оборачивает его в конструктор Just.

ПРИМЕЧАНИЕ. Функция 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, сигнализируя о неудаче.

 
 

Давайте опробуем этих деток:

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 в ка- честве аппликативного функтора. Если вы попробуете так сделать, то застрянете, поскольку аппликативные функторы не очень-то позволяют аппликативным значениям взаимодействовать друг с другом. Их в лучшем случае можно использовать как параметры для функции, применяя аппликативный стиль.

Аппликативные операторы извлекут свои результаты и переда- дут их функции в соответствующем для каждого аппликативного функтора виде, а затем соберут окончательное аппликативное зна- чение, но взаимодействие между ними не особенно заметно. Здесь, однако, каждый шаг зависит от результата предыдущего шага. Во время каждого приземления возможный результат предыдущего шага исследуется, а шест проверяется на равновесие. Это опреде- ляет, окончится ли посадка успешно либо неуспешно.

 

Банан на канате

Давайте разработаем функцию, которая игнорирует текущее коли- чество птиц на балансировочном шесте и просто заставляет Пьера поскользнуться и упасть. Мы на- зовём её banana:


 

 
 

banana:: Pole –> Maybe Pole banana _ = Nothing

Мы можем поместить эту функцию в цепочку вместе с нашими приземлениями птиц. Она всегда будет вызывать падение канато- ходца, поскольку игнорирует всё, что ей передаётся в качестве па- раметра, и неизменно возвращает неудачу.

 
 

ghci> return (0, 0) >>= landLeft 1 >>= banana >>= landRight 1 Nothing

Функции banana передаётся значение Just (1, 0), но она всегда производит значение Nothing, которое заставляет всё выражение возвращать в результате Nothing. Какая досада!..

 
 

Вместо создания функций, которые игнорируют свои входные данные и просто возвращают предопределённое монадическое значение, мы можем использовать функцию >>. Вот её реализация по умолчанию:

(>>):: (Monad m) => m a –> m b –> m b m >> n = m >>= \_ –> n

 
 

Обычно передача какого-либо значения функции, которая игнорирует свой параметр и всегда возвращает некое предопре- делённое значение, всегда даёт в результате это предопределённое значение. При использовании монад, однако, нужно принимать во внимание их контекст и значение. Вот как функция >> действует при использовании с типом Maybe:

ghci> Nothing >> Just 3 Nothing

ghci> Just 3 >> Just 4

Just 4

 
 

ghci> Just 3 >> Nothing Nothing

Если мы заменим оператор >> на вызов >>= \_ –>, легко увидеть, что происходит.

Мы можем заменить нашу функцию banana в цепочке на опера- тор >> и следующее за ним значение Nothing, чтобы получить гаран- тированную и очевидную неудачу:


 

 
 

ghci> return (0, 0) >>= landLeft 1 >> Nothing >>= landRight 1 Nothing

Как бы это выглядело, если бы мы не сделали разумный выбор, обработав значения типа Maybe как значения с контекстом неудачи и передав их функциям? Вот какой была бы последовательность приземлений птиц:

routine:: Maybe Pole

routine = case landLeft 1 (0, 0) of Nothing –> Nothing

Just pole1 –> case landRight 4 pole1 of Nothing –> Nothing

Just pole2 –> case landLeft 2 pole2 of Nothing –> Nothing

 
 

Just pole3 –> landLeft 1 pole3

Мы усаживаем птицу слева, а затем проверяем вероятность неудачи и вероятность успеха. В случае неудачи мы возвраща- ем значение Nothing. В случае успеха усаживаем птиц справа, а затем повторяем всё сызнова. Превращение этого убожества в симпатичную цепочку мона- дических применений с исполь- зованием функции >>= является классическим примером того, как монада Maybe экономит массу времени, когда вам необходимо последовательно выполнить вы-

числения, основанные на вычислениях, которые могли окончить- ся неуспешно.

Обратите внимание, каким образом реализация операции >>= для типа Maybe отражает именно эту логику, когда проверяется, рав- но ли значение Nothing, и действие производится на основе этих сведений. Если значение равно Nothing, она незамедлительно воз- вращает результат Nothing. Если значение не равно Nothing, она про- должает работу с тем, что находится внутри конструктора Just.


 

В этом разделе мы рассмотрели, как некоторые функции ра- ботают лучше, когда возвращаемые ими значения поддерживают неудачу. Превращая эти значения в значения типа Maybe и заменяя обычное применение функций вызовом операции >>=, мы практи- чески даром получили механизм обработки вычислений, которые могут оканчиваться неудачно. Причина в том, что операция >>= должна сохранять контекст значения, к которому она применяет функции. В данном случае контекстом являлось то, что наши зна- чения были значениями с неуспехом в вычислениях. Поэтому ког- да мы применяли к таким значениям функции, всегда учитывалась вероятность неуспеха.

 

Нотация do

Монады в языке Haskell настолько полезны, что они обзавелись своим собственным синтаксисом, который называется «нотация do». Вы уже познакомились с нотацией do в главе 8, когда мы ис- пользовали её для объединения нескольких действий ввода-выво- да. Как оказывается, нотация do предназначена не только для сис- темы ввода-вывода, но может использоваться для любой монады. Её принцип остаётся прежним: последовательное «склеивание» монадических значений.

 
 

Рассмотрим этот знакомый пример монадического применения:



Поделиться:


Последнее изменение этой страницы: 2017-02-17; просмотров: 171; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 54.144.233.198 (0.162 с.)