Haskell/Monad transformers: различия между версиями

Содержимое удалено Содержимое добавлено
typo fix
орфография, викификатор
Строка 1:
{{Haskell minitoc|chapter=Monads}}
 
К этому моменту вы должны были предварительно уяснить понятие монады, а также то, что различные монады используются еще для: <code>IO</code> для "«нечистых"» функций, <code>Maybe</code> для значений, которые могут быть или нет, и так далее.
С помощью монад, обеспечивающих такую полезную функциональность общего назначения, очень естественно, что порой мы хотели бы использовать возможности ''нескольких'' монад сразу -- — например, функция, которая использует и <code>IO</code>, и обработку исключений <code>Maybe</code>. Конечно, мы можем использовать такой тип как <code>IO (Maybe a)</code>, но это заставляет нас делать сопоставление с образцом в <code>do</code>-блоках, чтобы извлечь необходимые значения: однако, фишка ''монад'' была также в том, чтобы избавиться от этого.
 
Итак, рассмотрим '''монадные трансформеры''': специальные типы, которые позволяют нам комбинировать две монады в одной, но разделяющие поведение обоих. Начнем с примера, чтобы проиллюстрировать, почему трансформеры являются полезными и посмотрим простой пример того, как они работают.
 
== Проверка пароля ==
 
Рассмотрим обычную проблему из реальной жизни для ИТ-специалистов всего мира: составим программу запроса от пользователей паролей, которые трудно угадать.
Типичная стратегия -- — заставить пользователя ввести пароль не короче минимальной длины, и по крайней мере одну букву, одну цифру (и с другими подобными раздражающими требованиями).
 
Функция получения пароля от пользователя на Haskell может выглядеть следующим образом:
Строка 23 ⟶ 22 :
</source>
 
В первую очередь, <code>getPassword</code> является <code>IO</code>-функцией, так как ей необходимо получить ввод от пользователя, ну и он не всегда будет возвращаться. Мы также используем <code>Maybe</code>, так как мы намерены возвращать <code>Nothing</code> в случае, если пароль не проходит условие <code>isValid</code>. Заметим, однако, что мы фактически не будем использовать <code>Maybe</code> здесь как монаду: <code>do</code>-блок находится в <code>IO</code>-монаде, и нам просто повезло получить <code>return</code> с <code>Maybe</code>-значением внутри.
 
Истинная мотивация для трансформеров монад состоит не только в том, чтобы было проще написать <code>getPassword</code> (что все же происходит), а, скорее, чтобы упростить все куски кода, в которых мы его используем. Наша программа получения пароля может быть продолжена так:
Строка 39 ⟶ 38 :
 
== Простой монадный трансформер: <code>MaybeT</code> ==
Для упрощения функции <code>getPassword</code> и всего кода, который использует ее, мы определим ''монадный трансформер'', который дает <code>IO</code>-монаде некоторые характеристики монады <code>Maybe</code>; мы будем называть его <code>MaybeT</code>, следуя соглашению, что трансформеры монад имеют "«<code>T</code>"», добавленное к названию монады, чьи характеристики они обеспечивают.
 
Для упрощения функции <code>getPassword</code> и всего кода, который использует ее, мы определим ''монадный трансформер'', который дает <code>IO</code>-монаде некоторые характеристики монады <code>Maybe</code>; мы будем называть его <code>MaybeT</code>, следуя соглашению, что трансформеры монад имеют "<code>T</code>", добавленное к названию монады, чьи характеристики они обеспечивают.
 
<code>MaybeT</code> является оберткой вокруг <code>m (Maybe a)</code>, где <code>m</code> может быть любой монадой (в нашем примере, мы заинтересованы в <code>IO</code>):
Строка 46 ⟶ 44 :
newtype (Monad m) => MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
</source>
<code>newtype</code> -- — это просто более эффективная альтернатива обычным декларациям <code>data</code> на случаи, когда есть только один конструктор. Так, <code>MaybeT</code> является конструктором типа, параметризованного более <code>m</code>, с конструктором данных, который также называется <code>MaybeT</code> и удобной функцией доступа <code>runMaybeT</code>, с помощью которой мы можем получить доступ к базовому (внутреннему) представлению.
 
Весь смысл монадных трансформаторов в том, что ''они сами монады'', и поэтому нам нужно сделать <code>MaybeT m</code> экземпляром класса <code>Monad</code>:
Строка 53 ⟶ 51 :
return = MaybeT . return . Just
</source>
<code>return</code>  осуществляется  с помощью <code>Just</code>, который вставляет (не указанное из-за частичного применения значение) в  монаду <code>Maybe</code>, затем общий  <code>return</code>, который в свою очередь вводит (полученное значение) в  <code>m</code>  (чем бы это ни было) и затем  конструктор <code>MaybeT</code>.
Также возможно (хотя менее приятно читается) написать  <code>return = MaybeT . return . return</code>.
 
<source lang="haskell">
Строка 62 ⟶ 60 :
Just value -> runMaybeT $ f value
</source>
Как и у всех монад, оператор связывания <code>bind</code> является сердцем трансформера, и самым важным фрагментом кода для понимания того, как он работает.  Как всегда, полезно иметь в виду сигнатуру типов.  Типом  оператора связывания <code>bind</code> для монады <code>MaybeT</code>  будет:
 
<source lang="haskell">
Строка 101 ⟶ 99 :
</source>
 
Последний класс, <code>MonadTrans</code>, реализует (обеспечивает) функцию <code>lift</code>, которая очень полезна для того, чтобы взять функции из монады <code>m</code> и поднять (принести) их внутрь монады <code>MaybeT m</code>, так чтобы мы смогли использовать их в <code>do</code>-блоке внутри монады <code>MaybeT m</code>.
 
=== Применение к парольному примеру ===
Со всем сказанным выше, вот то, на что парольное управление будет похоже:
 
Со всем сказанным выше, вот то, на что парольное управление будет похоже:
<source lang="haskell">
getValidPassword :: MaybeT IO String
Строка 119 ⟶ 116 :
Этот код не прост, особенно в пользовательской функции <code>askPassword</code>. Более важно, что мы не должны вручную проверять, является ли результат <code>Nothing</code> или <code>Just</code>: оператор <code>bind</code> сделает это за нас.
 
Отметим как мы используем <code>lift</code>, чтобы поднять функции <code>getLine</code> и <code>putStrLn</code> вовнутрь монады <code>MaybeT IO</code>. Также, так как <code>MaybeT IO</code> является воплощением <code>MonadPlus</code>, проверка правильности пароля может быть осуществлена выражением <code>guard</code>, который вернет <code>mzero</code> (т.е.то есть <code>IO Nothing</code>) в случае плохого пароля.
 
И кстати, с помощью <code>MonadPlus</code> стало очень просто спрашивать у пользователя правильный пароль ''до бесконечности''
<source lang="haskell">
askPassword :: MaybeT IO ()
Строка 130 ⟶ 127 :
 
== Изобилие трансформеров ==
Модуль пакета <tt>transformers</tt> обеспечивает трансформеры для многих общих монад (<code>MaybeT</code>, например, может быть найдена в {{Haskell lib|p=transformers|v=latest|Control|Monad|Trans|Maybe}}). Они определены в соответствии с их нетрансформерской версией; т.е.то есть, реализация в базовом та же самая, только с дополнительными обертками и развертками, необходимыми для ввинчивания в другую монаду.
 
Модуль пакета <tt>transformers</tt> обеспечивает трансформеры для многих общих монад (<code>MaybeT</code>, например, может быть найдена в {{Haskell lib|p=transformers|v=latest|Control|Monad|Trans|Maybe}}). Они определены в соответствии с их нетрансформерской версией; т.е., реализация в базовом та же самая, только с дополнительными обертками и развертками, необходимыми для ввинчивания в другую монаду.
 
Выберем произвольный пример, <code>ReaderT Env IO String<code> -- вычисление, которое вовлечет считываемое значение из некоторого окружения типа <code>Env</code> (семантика из <code>Reader</code>, базовой монады) и выполняет некоторое <code>IO</code> (действие) для того, чтобы получить значение типа <code>String</code>. Так как операторы <code>bind</code> и <code>return</code> отражают семантику базовой монады, <code>do</code>-блок типа <code>ReaderT Env IO String</code> будет с внешней стороны похож на <code>do</code>-блок монады <code>Reader</code>; главная разница будет в том, что <code>IO</code>-действия становятся тривиальными для встраивания при использовании функции <code>lift</code>.
 
=== Манипуляции с типами ===
Мы видели, что конструктор типа для <code>MaybeT</code> является оболочкой для значения типа <code>Maybe</code> во внутренней монаде, и поэтому соответствующая функция доступа <code>runMaybeT</code> дает нам значение типа <code>m&nbsp;(Maybe a)</code> — --то т.е.есть, значение базовой монады возвращенную во внутреннюю монаду. Аналогичным образом, мы имеем
 
Мы видели, что конструктор типа для <code>MaybeT</code> является оболочкой для значения типа <code>Maybe</code> во внутренней монаде, и поэтому соответствующая функция доступа <code>runMaybeT</code> дает нам значение типа <code>m&nbsp;(Maybe a)</code> -- т.е., значение базовой монады возвращенную во внутреннюю монаду. Аналогичным образом, мы имеем
<source lang = "haskell">
runListT :: ListT m a -> m [a]
Строка 147 ⟶ 142 :
для трансформеров списков (list) и ошибок (error).
 
Все же, не все трансформеры связаны с базовой монадой таким образом. Монады <code>Writer</code>, <code>Reader</code>, <code>State</code> и <code>Cont</code> объединяет то, что в отличие от базовых монады в примерах выше, они не имеют ни нескольких конструкторов, ни конструкторов с несколькими аргументами. По этой причине, у них есть функции <tt>run...</tt>, которые действуют как простые развертыватели (unwrappers) аналогично соотвественнымсоответственным <tt>run...T</tt> из версий трансформера. В приведенной ниже таблице показаны результаты типов функций <tt>run...</tt> и <tt>run...T</tt> в каждом конкретном случае, что может рассматриваться как типы обернутые базовой и трансформированной монадой соответственно.
 
{|align="center" border="1" cellpadding="3"
|- bgcolor="#cc9999" rowspan="2"
!|Base Monad
!|Transformer
!|Original Type<br/><small>("«wrapped"» by base)</small>
!|Combined Type<br/><small>("«wrapped"» by transformed)</small>
|-
||Writer ||WriterT || <code>(a, w)</code> || <code>m (a, w)</code>
Строка 165 ⟶ 160 :
|}
 
Первое, на что нужно обратить внимание, это то, что базовые монады нигде не было видно в комбинированной типов. Это очень естественно, так как без конструкторы интересны (например, те для Может быть, или списки) нет никаких причин, чтобы сохранить базовый тип монады после разворачивания преобразованной монады. . Кроме того, в трех последних случаях у нас есть функция типа завернутые StateT , например, превращает государственные преобразования функций вида с -> (A, S) в государственно-преобразования функций вида с -> M ( , с) , так что только тип результата функции завернутые идет во внутренний монады. ReaderT аналогична, но не ContT : в связи с семантикой Cont ( продолжение монады) результат обоих типов завернутые функции и ее функциональное Аргумент должен быть таким же, так что трансформатор ставит как во внутреннюю монады. Что эти примеры показывают, что в целом нет никакой волшебной формулы для создания трансформатора версия монады-формы каждого трансформера, зависит от того, что имеет смысл в контексте его не-трансформатор типа.
 
== Подъем ==
 
Функция <code>lift</code>, которую мы впервые представили во введении, очень важна для повседневного использования трансформеров; и поэтому мы посвятим несколько слов ей.
 
=== liftM ===
Начнем с того, что строго говоря, она не из темы монадных трансформеров. <code>liftM</code> -- — черезвычайно полезная функция в стандартной библиотеке со следующей сигнатурой:
 
Начнем с того, что строго говоря, она не из темы монадных трансформеров. <code>liftM</code> -- черезвычайно полезная функция в стандартной библиотеке со следующей сигнатурой:
 
<source lang="haskell">
Строка 179 ⟶ 172 :
</source>
 
<code>liftM</code> применяет функцию <code>(a1 -> r)</code> к значению в рамках (внутри) монады <code>m</code>. Если вы предпочитаете бесточечную запись, она может превратить обычную функцию в такую, которая действует внутри <code>m</code> -- — и ''это'' как раз то, что подразумевается под поднятием в монаду.
 
Давайте посомтрим, как <code>liftM</code> используется. Следующие фрагменты обозначают одно и то же.
Строка 224 ⟶ 217 :
 
=== lift ===
 
Когда мы используем монады, созданные с помощью монадных трансформеров, мы можем избежать явного управления внутренними монадными типами, и в результате получаем более ясный и простой код. Вместо создания дополнительных <code>do</code>-блоков внутри вычисления для манипуляции значениями во внутренней монаде, мы можем использовать поднятые операции, чтобоы перенести функции из внутренней монады в комбинированную монаду.
 
С <code>liftM</code> мы увидели, что сущность поднятия -- — перефразируя документацию -- — в продвижении чего-то в монаду. Функция <code>lift</code>, доступная для всех монадных трансформеров, выполняет разный тип поднятия: она продвигает вычисление из внутренней монады в комбинированную монаду. Функция <code>lift</code> определена как единственный метод класса <code>MonadTrans</code> в {{Haskell lib|p=transformers|v=latest|Control|Monad|Trans|Class}}.
 
<source lang="haskell">
Строка 241 ⟶ 233 :
</source>
 
<code>liftIO</code> может быть удобен, когда у нас множестов трансформеров помещены друг за другом (как в стек) в одну комбинированную монаду. В подобных случаях, <code>IO</code> будет всегда самой внутренней монадой, и таким образом обычно нужен более чем один <code>lift</code>, чтобы поднять <code>IO</code>-значения на вершину стека. <code>liftIO</code>, однако, определен для воплощений таким образом, что позволяет нам поднять <code>IO</code>-значение из произвольной глубины, написав функцию лишь единожды.
 
=== Реализация <code>lift</code> ===
 
Реализация <code>lift</code> как правило довольно прямолинейна (проста). Рассмотрим трансформер <code>MaybeT</code>:
 
Строка 252 ⟶ 243 :
</source>
 
Мы начинаем с монадическим значением внутренней монады -- — средним слоем, если вы предпочитаете аналогию с сэндвичем. Используя оператор <code>bind</code> и конструктор типа для базовой монады, мы плавно сдвигаем (скатываем, намазываем ??) нижний слой (базовую монаду) под средний слой. В конце, мы помещаем верхний срез нашего сэндвича с помощью конструктора <code>MaybeT</code>. Таким образом, используя фнукцию <code>lift</code>, мы трансформировали нижний кусок начинки сэндвича в подлинно трехслойный монадический сэндвич. Отметим, что как в реализации класса <code>Monad</code>, и оператор <code>bind</code>, и общий (основной) оператор <code>return</code> работают в границах внутренней монады.
 
{{Exercises|1=
Строка 261 ⟶ 252 :
 
== Реализация трансформеров ==
 
Для того, чтобы развить лучшее понимание работы трансформеров, мы обсудим две реализации в стандартных библиотеках.
 
=== Трансформер List ===
 
Также как с трансформером <code>Maybe</code>, мы начнем с создания конструктора типа, который принимает один аргумент:
<source lang="haskell">
Строка 271 ⟶ 260 :
</source>
 
Реализация монады <code>ListT m</code> поразительно похожа на свою "«кузину"», монаду списков. Мы делаем те же самые операции
внутри внутренней монады <code>m</code>, пакуем и распаковываем монадический сэндвич.
 
Строка 301 ⟶ 290 :
}}
 
=== Трансформер State ===
 
В прошлый раз мы пристально рассмотрели реализацию двух простых монадных трансформеров, <code>MaybeT</code> и <code>ListT</code>, совершив обходной путь, чтобы обсудить подъем из (простой) монады в ее вариант-трансформер. Теперь, соединим две идеи вместе, внимательно рассмотрев реализацию одного из наиболее интересных трансформеров в стандартной библиотеке, <code>StateT</code>. Изучение этого трансформера сотворит озарение в понимании механизма трансформеров, которое вы сможете призвать впоследствии, когда будуте использовать монадные трансформеры в вашем коде. Прежде чем продолжить, вам может быть понадобиться освежить в памяти или просмотреть [[Haskell/Understanding monads/State|State monad]].
 
Как и монада <code>State</code> могла быть построена определением <code>newtype State s a = State { runState :: (s -> (a,s)) }</code> <ref>В версии пакета <tt>mtl</tt> ранее 2.0.0.0, так и ''было'' построено. В настоящее время, однако, <code>State s</code> реализована как синоним типа для <code>StateT s Identity</code>.</ref> Трансформер <code>StateT</code> создан определением
<source lang="haskell">
newtype StateT s m a = StateT { runStateT :: (s -> m (a,s)) }</source>
Строка 341 ⟶ 329 :
Наше определение <code>return</code> использует функцию <code>return</code> внутренней монады, и оператор связывания использует do-блок, чтобы выполнить вычисление во внутренней монаде.
 
Мы также хотим декларировать все комбинированные монады, которые используют трансформер <code>StateT</code>, как воплощения
класса <code>MonadState</code>, так что мы дадим определения <code>get</code> и <code>put</code>:
 
Строка 357 ⟶ 345 :
</source>
 
Последним шагом сделаем наш монадный трансформер полностью интегрированным с классом монад Haskell'а --Haskell’а — для этого сделаем <code>StateT s</code> воплощением класса <code>MonadTrans</code>, обеспечив функцию <code>lift</code>:
<source lang="haskell">
instance MonadTrans (StateT s) where
Строка 363 ⟶ 351 :
</source>
 
Функция <code>lift</code> создает функцию, изменяющую состояние, <code>StateT</code>, которая связывает вычисление во внутренней монаде с функцией, пакующей результат со входным состоянием. Результат в том, если для воплощения мы применяем
StateT к монаде the List, функция, которая возвращает список (т.е.то есть, вычисление в монаде List) может быть поднято вовнутрь
<code>StateT&nbsp;s&nbsp;[]</code>, где оно станет функцией, которая возвращает <code>StateT&nbsp;(s&nbsp;->&nbsp;[(a,s)])</code>. Таким образом, поднятое вычисление производит ''множественные'' пары (значение, состояние) из его внутреннего состояния. Эффект выразится в "«разветвлении"» вычисления в StateT, создавая разные ветви для разных значений в списке, возращенном поднятой функцией. Разумеется, применяя <code>StateT</code> к разным монадам, получим разную семантику функции <code>lift</code>.
 
== Благодарности ==
Этот модуль использует ряд отрывков из [http://www.haskell.org/haskellwiki/All_About_Monads All About Monads], с разрешения автора Jeff Newbern.