Заглавная страница Избранные статьи Случайная статья Познавательные статьи Новые добавления Обратная связь КАТЕГОРИИ: АрхеологияБиология Генетика География Информатика История Логика Маркетинг Математика Менеджмент Механика Педагогика Религия Социология Технологии Физика Философия Финансы Химия Экология ТОП 10 на сайте Приготовление дезинфицирующих растворов различной концентрацииТехника нижней прямой подачи мяча. Франко-прусская война (причины и последствия) Организация работы процедурного кабинета Смысловое и механическое запоминание, их место и роль в усвоении знаний Коммуникативные барьеры и пути их преодоления Обработка изделий медицинского назначения многократного применения Образцы текста публицистического стиля Четыре типа изменения баланса Задачи с ответами для Всероссийской олимпиады по праву Мы поможем в написании ваших работ! ЗНАЕТЕ ЛИ ВЫ?
Влияние общества на человека
Приготовление дезинфицирующих растворов различной концентрации Практические работы по географии для 6 класса Организация работы процедурного кабинета Изменения в неживой природе осенью Уборка процедурного кабинета Сольфеджио. Все правила по сольфеджио Балочные системы. Определение реакций опор и моментов защемления |
Совершенствуем наши аппликативные функторы
Когда мы начали с функторов, вы видели, что можно отоб- ражать разные типы данных с помощью функций, используя класс типов 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; просмотров: 169; Нарушение авторского права страницы; Мы поможем в написании вашей работы! infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.144.182.250 (0.197 с.) |