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

Материал из Викиучебника — открытых книг для открытого мира
Содержимое удалено Содержимое добавлено
→‎Реализация lift: орфография
м <source> -> <syntaxhighlight> (phab:T237267)
Строка 11: Строка 11:


Функция получения пароля от пользователя на Haskell может выглядеть следующим образом:
Функция получения пароля от пользователя на Haskell может выглядеть следующим образом:
<source lang="haskell">
<syntaxhighlight lang="haskell">
getPassword :: IO (Maybe String)
getPassword :: IO (Maybe String)
getPassword = do s <- getLine
getPassword = do s <- getLine
Строка 20: Строка 20:
isValid :: String -> Bool
isValid :: String -> Bool
isValid s = length s >= 8 && any isAlpha s && any isNumber s && any isPunctuation s
isValid s = length s >= 8 && any isAlpha s && any isNumber s && any isPunctuation s
</syntaxhighlight>
</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> является <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> (что все же происходит), а, скорее, чтобы упростить все куски кода, в которых мы его используем. Наша программа получения пароля может быть продолжена так:
Истинная мотивация для трансформеров монад состоит не только в том, чтобы было проще написать <code>getPassword</code> (что все же происходит), а, скорее, чтобы упростить все куски кода, в которых мы его используем. Наша программа получения пароля может быть продолжена так:
<source lang="haskell">
<syntaxhighlight lang="haskell">
askPassword :: IO ()
askPassword :: IO ()
askPassword = do putStrLn "Insert your new password:"
askPassword = do putStrLn "Insert your new password:"
Строка 32: Строка 32:
then do putStrLn "Storing in database..."
then do putStrLn "Storing in database..."
-- ... other stuff, including 'else'
-- ... other stuff, including 'else'
</syntaxhighlight>
</source>
Нам нужна одна строка для создания <code>maybe_value</code>-переменной, а затем мы должны сделать некоторые дополнительные проверки, чтобы выяснить в порядке наш пароль или нет.
Нам нужна одна строка для создания <code>maybe_value</code>-переменной, а затем мы должны сделать некоторые дополнительные проверки, чтобы выяснить в порядке наш пароль или нет.


Строка 41: Строка 41:


<code>MaybeT</code> является оберткой вокруг <code>m (Maybe a)</code>, где <code>m</code> может быть любой монадой (в нашем примере, мы заинтересованы в <code>IO</code>):
<code>MaybeT</code> является оберткой вокруг <code>m (Maybe a)</code>, где <code>m</code> может быть любой монадой (в нашем примере, мы заинтересованы в <code>IO</code>):
<source lang="haskell">
<syntaxhighlight lang="haskell">
newtype (Monad m) => MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
newtype (Monad m) => MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
</syntaxhighlight>
</source>
<code>newtype</code> — это просто более эффективная альтернатива обычным декларациям <code>data</code> на случаи, когда есть только один конструктор. Так, <code>MaybeT</code> является конструктором типа, параметризованного более <code>m</code>, с конструктором данных, который также называется <code>MaybeT</code> и удобной функцией доступа <code>runMaybeT</code>, с помощью которой мы можем получить доступ к базовому (внутреннему) представлению.
<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>:
Весь смысл монадных трансформаторов в том, что ''они сами монады'', и поэтому нам нужно сделать <code>MaybeT m</code> экземпляром класса <code>Monad</code>:
<source lang="haskell">
<syntaxhighlight lang="haskell">
instance Monad m => Monad (MaybeT m) where
instance Monad m => Monad (MaybeT m) where
return = MaybeT . return . Just
return = MaybeT . return . Just
</syntaxhighlight>
</source>
<code>return</code> осуществляется с помощью <code>Just</code>, который вставляет (не указанное из-за частичного применения значение) в монаду <code>Maybe</code>, затем общий <code>return</code>, который в свою очередь вводит (полученное значение) в <code>m</code> (чем бы это ни было) и затем конструктор <code>MaybeT</code>.
<code>return</code> осуществляется с помощью <code>Just</code>, который вставляет (не указанное из-за частичного применения значение) в монаду <code>Maybe</code>, затем общий <code>return</code>, который в свою очередь вводит (полученное значение) в <code>m</code> (чем бы это ни было) и затем конструктор <code>MaybeT</code>.
Также возможно (хотя менее приятно читается) написать <code>return = MaybeT . return . return</code>.
Также возможно (хотя менее приятно читается) написать <code>return = MaybeT . return . return</code>.


<source lang="haskell">
<syntaxhighlight lang="haskell">
x >>= f = MaybeT $ do maybe_value <- runMaybeT x
x >>= f = MaybeT $ do maybe_value <- runMaybeT x
case maybe_value of
case maybe_value of
Nothing -> return Nothing
Nothing -> return Nothing
Just value -> runMaybeT $ f value
Just value -> runMaybeT $ f value
</syntaxhighlight>
</source>
Как и у всех монад, оператор связывания <code>bind</code> является сердцем трансформера, и самым важным фрагментом кода для понимания того, как он работает. Как всегда, полезно иметь в виду сигнатуру типов. Типом оператора связывания <code>bind</code> для монады <code>MaybeT</code> будет:
Как и у всех монад, оператор связывания <code>bind</code> является сердцем трансформера, и самым важным фрагментом кода для понимания того, как он работает. Как всегда, полезно иметь в виду сигнатуру типов. Типом оператора связывания <code>bind</code> для монады <code>MaybeT</code> будет:


<source lang="haskell">
<syntaxhighlight lang="haskell">
-- The signature of (>>=), specialized to MaybeT m
-- The signature of (>>=), specialized to MaybeT m
(>>=) :: MaybeT m a -> (a -> MaybeT m b) -> MaybeT m b
(>>=) :: MaybeT m a -> (a -> MaybeT m b) -> MaybeT m b
</syntaxhighlight>
</source>
Теперь, давайте рассмотрим, что он делает, шаг за шагом, начиная с первой строки <code>do</code>-блока.
Теперь, давайте рассмотрим, что он делает, шаг за шагом, начиная с первой строки <code>do</code>-блока.


Строка 75: Строка 75:
* Наконец, <code>do</code>-блок в целом имеет тип <code>m (Maybe b)</code>; так он обернут конструктором <code>MaybeT</code>. Это может выглядеть сложновато, но все же менее сложно, чем куча оберток и распаковок, реализация делает тоже самое, что и знакомый оператор <code>bind</code> монады <code>Maybe</code>:
* Наконец, <code>do</code>-блок в целом имеет тип <code>m (Maybe b)</code>; так он обернут конструктором <code>MaybeT</code>. Это может выглядеть сложновато, но все же менее сложно, чем куча оберток и распаковок, реализация делает тоже самое, что и знакомый оператор <code>bind</code> монады <code>Maybe</code>:


<source lang="haskell">
<syntaxhighlight lang="haskell">
-- (>>=) for the Maybe monad
-- (>>=) for the Maybe monad
maybe_value >>= f = case maybe_value of
maybe_value >>= f = case maybe_value of
Nothing -> Nothing
Nothing -> Nothing
Just value -> f value
Just value -> f value
</syntaxhighlight>
</source>


Вы можете удивиться, почему мы используем конструктор <code>MaybeT</code> перед <code>do</code>-блоком, когда внутри него мы используем функцию доступа <code>runMaybeT</code>: однако, <code>do</code>-блок должен быть в монаде <code>m</code>, а не в <code>MaybeT m</code>, так как для последней мы еще не определили оператор связывания <code>bind</code>.
Вы можете удивиться, почему мы используем конструктор <code>MaybeT</code> перед <code>do</code>-блоком, когда внутри него мы используем функцию доступа <code>runMaybeT</code>: однако, <code>do</code>-блок должен быть в монаде <code>m</code>, а не в <code>MaybeT m</code>, так как для последней мы еще не определили оператор связывания <code>bind</code>.
Строка 87: Строка 87:


Технически, это все, что нам надо; однако, будет удобно сделать <code>MaybeT</code> воплощением еще некоторых других классов:
Технически, это все, что нам надо; однако, будет удобно сделать <code>MaybeT</code> воплощением еще некоторых других классов:
<source lang="haskell">
<syntaxhighlight lang="haskell">
instance Monad m => MonadPlus (MaybeT m) where
instance Monad m => MonadPlus (MaybeT m) where
mzero = MaybeT $ return Nothing
mzero = MaybeT $ return Nothing
Строка 97: Строка 97:
instance MonadTrans MaybeT where
instance MonadTrans MaybeT where
lift = MaybeT . (liftM Just)
lift = MaybeT . (liftM Just)
</syntaxhighlight>
</source>


Последний класс, <code>MonadTrans</code>, реализует (обеспечивает) функцию <code>lift</code>, которая очень полезна для того, чтобы взять функции из монады <code>m</code> и поднять (принести) их внутрь монады <code>MaybeT m</code>, так чтобы мы смогли использовать их в <code>do</code>-блоке внутри монады <code>MaybeT m</code>.
Последний класс, <code>MonadTrans</code>, реализует (обеспечивает) функцию <code>lift</code>, которая очень полезна для того, чтобы взять функции из монады <code>m</code> и поднять (принести) их внутрь монады <code>MaybeT m</code>, так чтобы мы смогли использовать их в <code>do</code>-блоке внутри монады <code>MaybeT m</code>.
Строка 103: Строка 103:
=== Применение к парольному примеру ===
=== Применение к парольному примеру ===
Со всем сказанным выше, вот то, на что парольное управление будет похоже:
Со всем сказанным выше, вот то, на что парольное управление будет похоже:
<source lang="haskell">
<syntaxhighlight lang="haskell">
getValidPassword :: MaybeT IO String
getValidPassword :: MaybeT IO String
getValidPassword = do s <- lift getLine
getValidPassword = do s <- lift getLine
Строка 113: Строка 113:
value <- getValidPassword
value <- getValidPassword
lift $ putStrLn "Storing in database..."
lift $ putStrLn "Storing in database..."
</syntaxhighlight>
</source>
Этот код не прост, особенно в пользовательской функции <code>askPassword</code>. Более важно, что мы не должны вручную проверять, является ли результат <code>Nothing</code> или <code>Just</code>: оператор <code>bind</code> сделает это за нас.
Этот код не прост, особенно в пользовательской функции <code>askPassword</code>. Более важно, что мы не должны вручную проверять, является ли результат <code>Nothing</code> или <code>Just</code>: оператор <code>bind</code> сделает это за нас.


Строка 119: Строка 119:


И кстати, с помощью <code>MonadPlus</code> стало очень просто спрашивать у пользователя правильный пароль ''до бесконечности''
И кстати, с помощью <code>MonadPlus</code> стало очень просто спрашивать у пользователя правильный пароль ''до бесконечности''
<source lang="haskell">
<syntaxhighlight lang="haskell">
askPassword :: MaybeT IO ()
askPassword :: MaybeT IO ()
askPassword = do lift $ putStrLn "Insert your new password:"
askPassword = do lift $ putStrLn "Insert your new password:"
value <- msum $ repeat getValidPassword
value <- msum $ repeat getValidPassword
lift $ putStrLn "Storing in database..."
lift $ putStrLn "Storing in database..."
</syntaxhighlight>
</source>


== Изобилие трансформеров ==
== Изобилие трансформеров ==
Строка 133: Строка 133:
=== Манипуляции с типами ===
=== Манипуляции с типами ===
Мы видели, что конструктор типа для <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">
<syntaxhighlight lang = "haskell">
runListT :: ListT m a -> m [a]
runListT :: ListT m a -> m [a]
</syntaxhighlight>
</source>
и
и
<source lang = "haskell">
<syntaxhighlight lang = "haskell">
runErrorT :: ErrorT e m a -> m (Either e a)
runErrorT :: ErrorT e m a -> m (Either e a)
</syntaxhighlight>
</source>
для трансформеров списков (list) и ошибок (error).
для трансформеров списков (list) и ошибок (error).


Строка 168: Строка 168:
Начнем с того, что строго говоря, она не из темы монадных трансформеров. <code>liftM</code> — черезвычайно полезная функция в стандартной библиотеке со следующей сигнатурой:
Начнем с того, что строго говоря, она не из темы монадных трансформеров. <code>liftM</code> — черезвычайно полезная функция в стандартной библиотеке со следующей сигнатурой:


<source lang="haskell">
<syntaxhighlight lang="haskell">
liftM :: Monad m => (a1 -> r) -> m a1 -> m r
liftM :: Monad m => (a1 -> r) -> m a1 -> m r
</syntaxhighlight>
</source>


<code>liftM</code> применяет функцию <code>(a1 -> r)</code> к значению в рамках (внутри) монады <code>m</code>. Если вы предпочитаете бесточечную запись, она может превратить обычную функцию в такую, которая действует внутри <code>m</code> — и ''это'' как раз то, что подразумевается под поднятием в монаду.
<code>liftM</code> применяет функцию <code>(a1 -> r)</code> к значению в рамках (внутри) монады <code>m</code>. Если вы предпочитаете бесточечную запись, она может превратить обычную функцию в такую, которая действует внутри <code>m</code> — и ''это'' как раз то, что подразумевается под поднятием в монаду.
Строка 182: Строка 182:
|-
|-
|valign="top"|
|valign="top"|
<source lang="haskell">
<syntaxhighlight lang="haskell">
do x <- monadicValue
do x <- monadicValue
return (f x)
return (f x)
</syntaxhighlight>
</source>
|valign="top"|
|valign="top"|
<source lang="haskell">
<syntaxhighlight lang="haskell">
liftM f monadicValue
liftM f monadicValue
</syntaxhighlight>
</source>
|valign="top"|
|valign="top"|
<source lang="haskell">
<syntaxhighlight lang="haskell">
f `liftM` monadicValue
f `liftM` monadicValue
</syntaxhighlight>
</source>
|}
|}


Строка 203: Строка 203:
|-
|-
||
||
<source lang="haskell">
<syntaxhighlight lang="haskell">
f $ value
f $ value
</syntaxhighlight>
</source>
||
||
<source lang="haskell">
<syntaxhighlight lang="haskell">
f `liftM` monadicValue
f `liftM` monadicValue
</syntaxhighlight>
</source>
|}
|}


Строка 221: Строка 221:
С <code>liftM</code> мы увидели, что сущность поднятия — перефразируя документацию — в продвижении чего-то в монаду. Функция <code>lift</code>, доступная для всех монадных трансформеров, выполняет разный тип поднятия: она продвигает вычисление из внутренней монады в комбинированную монаду. Функция <code>lift</code> определена как единственный метод класса <code>MonadTrans</code> в {{Haskell lib|p=transformers|v=latest|Control|Monad|Trans|Class}}.
С <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">
<syntaxhighlight lang="haskell">
class MonadTrans t where
class MonadTrans t where
lift :: (Monad m) => m a -> t m a
lift :: (Monad m) => m a -> t m a
</syntaxhighlight>
</source>


Имеется вариант <code>lift</code>, специфичный для <code>IO</code> и называемый <code>liftIO</code>, который является единственным методом класса <code>MonadIO</code> в {{Haskell lib|p=transformers|v=latest|Control|Monad|IO|Class}}.
Имеется вариант <code>lift</code>, специфичный для <code>IO</code> и называемый <code>liftIO</code>, который является единственным методом класса <code>MonadIO</code> в {{Haskell lib|p=transformers|v=latest|Control|Monad|IO|Class}}.


<source lang="haskell">
<syntaxhighlight lang="haskell">
class (Monad m) => MonadIO m where
class (Monad m) => MonadIO m where
liftIO :: IO a -> m a
liftIO :: IO a -> m a
</syntaxhighlight>
</source>


<code>liftIO</code> может быть удобен, когда у нас множестов трансформеров помещены друг за другом (как в стек) в одну комбинированную монаду. В подобных случаях, <code>IO</code> будет всегда самой внутренней монадой, и таким образом обычно нужен более чем один <code>lift</code>, чтобы поднять <code>IO</code>-значения на вершину стека. <code>liftIO</code>, однако, определен для воплощений таким образом, что позволяет нам поднять <code>IO</code>-значение из произвольной глубины, написав функцию лишь единожды.
<code>liftIO</code> может быть удобен, когда у нас множестов трансформеров помещены друг за другом (как в стек) в одну комбинированную монаду. В подобных случаях, <code>IO</code> будет всегда самой внутренней монадой, и таким образом обычно нужен более чем один <code>lift</code>, чтобы поднять <code>IO</code>-значения на вершину стека. <code>liftIO</code>, однако, определен для воплощений таким образом, что позволяет нам поднять <code>IO</code>-значение из произвольной глубины, написав функцию лишь единожды.
Строка 238: Строка 238:
Реализация <code>lift</code> как правило довольно прямолинейна (проста). Рассмотрим трансформер <code>MaybeT</code>:
Реализация <code>lift</code> как правило довольно прямолинейна (проста). Рассмотрим трансформер <code>MaybeT</code>:


<source lang="haskell">
<syntaxhighlight lang="haskell">
instance MonadTrans MaybeT where
instance MonadTrans MaybeT where
lift m = MaybeT (m >>= return . Just)
lift m = MaybeT (m >>= return . Just)
</syntaxhighlight>
</source>


Мы начинаем с монадическим значением внутренней монады — средним слоем, если вы предпочитаете аналогию с сэндвичем. Используя оператор <code>bind</code> и конструктор типа для базовой монады, мы плавно сдвигаем (скатываем, намазываем ??) нижний слой (базовую монаду) под средний слой. В конце, мы помещаем верхний срез нашего сэндвича с помощью конструктора <code>MaybeT</code>. Таким образом, используя функцию <code>lift</code>, мы трансформировали нижний кусок начинки сэндвича в подлинно трехслойный монадический сэндвич. Отметим, что как в реализации класса <code>Monad</code>, и оператор <code>bind</code>, и общий (основной) оператор <code>return</code> работают в границах внутренней монады.
Мы начинаем с монадическим значением внутренней монады — средним слоем, если вы предпочитаете аналогию с сэндвичем. Используя оператор <code>bind</code> и конструктор типа для базовой монады, мы плавно сдвигаем (скатываем, намазываем ??) нижний слой (базовую монаду) под средний слой. В конце, мы помещаем верхний срез нашего сэндвича с помощью конструктора <code>MaybeT</code>. Таким образом, используя функцию <code>lift</code>, мы трансформировали нижний кусок начинки сэндвича в подлинно трехслойный монадический сэндвич. Отметим, что как в реализации класса <code>Monad</code>, и оператор <code>bind</code>, и общий (основной) оператор <code>return</code> работают в границах внутренней монады.
Строка 256: Строка 256:
=== Трансформер List ===
=== Трансформер List ===
Также как с трансформером <code>Maybe</code>, мы начнем с создания конструктора типа, который принимает один аргумент:
Также как с трансформером <code>Maybe</code>, мы начнем с создания конструктора типа, который принимает один аргумент:
<source lang="haskell">
<syntaxhighlight lang="haskell">
newtype ListT m a = ListT { runListT :: m [a] }
newtype ListT m a = ListT { runListT :: m [a] }
</syntaxhighlight>
</source>


Реализация монады <code>ListT m</code> поразительно похожа на свою «кузину», монаду списков. Мы делаем те же самые операции
Реализация монады <code>ListT m</code> поразительно похожа на свою «кузину», монаду списков. Мы делаем те же самые операции
Строка 268: Строка 268:
|-
|-
|valign="top"|
|valign="top"|
<source lang="haskell">
<syntaxhighlight lang="haskell">
instance Monad [] where
instance Monad [] where
xs >>= f =
xs >>= f =
Строка 274: Строка 274:
let yss = map f xs
let yss = map f xs
in concat yss
in concat yss
</syntaxhighlight>
</source>
|valign="top"|
|valign="top"|
<source lang="haskell">
<syntaxhighlight lang="haskell">
instance (Monad m) => Monad (ListT m) where
instance (Monad m) => Monad (ListT m) where
tm >>= f = ListT $ runListT tm
tm >>= f = ListT $ runListT tm
>>= \xs -> mapM (runListT . f) xs
>>= \xs -> mapM (runListT . f) xs
>>= \yss -> return (concat yss)
>>= \yss -> return (concat yss)
</syntaxhighlight>
</source>
|}
|}


Строка 294: Строка 294:


Как и монада <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> создан определением
Как и монада <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">
<syntaxhighlight lang="haskell">
newtype StateT s m a = StateT { runStateT :: (s -> m (a,s)) }</source>
newtype StateT s m a = StateT { runStateT :: (s -> m (a,s)) }</syntaxhighlight>


<code>State&nbsp;s</code> является воплощением как класса <code>Monad</code>, так и класса <code>MonadState&nbsp;s</code> (который обеспечивает <code>get</code> и <code>put</code>), так что <code>StateT&nbsp;s&nbsp;m</code> должна быть членом классов <code>Monad</code> и <code>MonadState&nbsp;s</code>. Более того, если <code>m</code> является воплощением <code>MonadPlus</code>, <code>StateT&nbsp;s&nbsp;m</code> также должна быть членом <code>MonadPlus</code>.
<code>State&nbsp;s</code> является воплощением как класса <code>Monad</code>, так и класса <code>MonadState&nbsp;s</code> (который обеспечивает <code>get</code> и <code>put</code>), так что <code>StateT&nbsp;s&nbsp;m</code> должна быть членом классов <code>Monad</code> и <code>MonadState&nbsp;s</code>. Более того, если <code>m</code> является воплощением <code>MonadPlus</code>, <code>StateT&nbsp;s&nbsp;m</code> также должна быть членом <code>MonadPlus</code>.
Строка 305: Строка 305:
!|StateT
!|StateT
|-valign="top"
|-valign="top"
||<source lang="haskell">
||<syntaxhighlight lang="haskell">
newtype State s a =
newtype State s a =
State { runState :: (s -> (a,s)) }
State { runState :: (s -> (a,s)) }
Строка 314: Строка 314:
let (v,s') = x s
let (v,s') = x s
in runState (f v) s'
in runState (f v) s'
</syntaxhighlight>
</source>
||<source lang="haskell">
||<syntaxhighlight lang="haskell">
newtype StateT s m a =
newtype StateT s m a =
StateT { runStateT :: (s -> m (a,s)) }
StateT { runStateT :: (s -> m (a,s)) }
Строка 324: Строка 324:
(v,s') <- x s -- get new value and state
(v,s') <- x s -- get new value and state
runStateT (f v) s' -- pass them to f
runStateT (f v) s' -- pass them to f
</syntaxhighlight>
</source>
|}
|}


Строка 332: Строка 332:
класса <code>MonadState</code>, так что мы дадим определения <code>get</code> и <code>put</code>:
класса <code>MonadState</code>, так что мы дадим определения <code>get</code> и <code>put</code>:


<source lang="haskell">
<syntaxhighlight lang="haskell">
instance (Monad m) => MonadState s (StateT s m) where
instance (Monad m) => MonadState s (StateT s m) where
get = StateT $ \s -> return (s,s)
get = StateT $ \s -> return (s,s)
put s = StateT $ \_ -> return ((),s)
put s = StateT $ \_ -> return ((),s)
</syntaxhighlight>
</source>


Наконец, мы хотим декларировать все комбинированные монады, в которых используется <code>StateT</code> с воплощением <code>MonadPlus</code>, как воплощения класса <code>MonadPlus</code>:
Наконец, мы хотим декларировать все комбинированные монады, в которых используется <code>StateT</code> с воплощением <code>MonadPlus</code>, как воплощения класса <code>MonadPlus</code>:
<source lang="haskell">
<syntaxhighlight lang="haskell">
instance (MonadPlus m) => MonadPlus (StateT s m) where
instance (MonadPlus m) => MonadPlus (StateT s m) where
mzero = StateT $ \s -> mzero
mzero = StateT $ \s -> mzero
(StateT x1) `mplus` (StateT x2) = StateT $ \s -> (x1 s) `mplus` (x2 s)
(StateT x1) `mplus` (StateT x2) = StateT $ \s -> (x1 s) `mplus` (x2 s)
</syntaxhighlight>
</source>


Последним шагом сделаем наш монадный трансформер полностью интегрированным с классом монад Haskell’а — для этого сделаем <code>StateT s</code> воплощением класса <code>MonadTrans</code>, обеспечив функцию <code>lift</code>:
Последним шагом сделаем наш монадный трансформер полностью интегрированным с классом монад Haskell’а — для этого сделаем <code>StateT s</code> воплощением класса <code>MonadTrans</code>, обеспечив функцию <code>lift</code>:
<source lang="haskell">
<syntaxhighlight lang="haskell">
instance MonadTrans (StateT s) where
instance MonadTrans (StateT s) where
lift c = StateT $ \s -> c >>= (\x -> return (x,s))
lift c = StateT $ \s -> c >>= (\x -> return (x,s))
</syntaxhighlight>
</source>


Функция <code>lift</code> создает функцию, изменяющую состояние, <code>StateT</code>, которая связывает вычисление во внутренней монаде с функцией, пакующей результат со входным состоянием. Результат в том, если для воплощения мы применяем
Функция <code>lift</code> создает функцию, изменяющую состояние, <code>StateT</code>, которая связывает вычисление во внутренней монаде с функцией, пакующей результат со входным состоянием. Результат в том, если для воплощения мы применяем

Версия от 16:11, 16 апреля 2020

Monad transformers (Solutions)
Monads

Шаблон:Haskell chapter/Monads

К этому моменту вы должны были предварительно уяснить понятие монады, а также то, что различные монады используются еще для: IO для «нечистых» функций, Maybe для значений, которые могут быть или нет, и так далее. С помощью монад, обеспечивающих такую полезную функциональность общего назначения, очень естественно, что порой мы хотели бы использовать возможности нескольких монад сразу — например, функция, которая использует и IO, и обработку исключений Maybe. Конечно, мы можем использовать такой тип как IO (Maybe a), но это заставляет нас делать сопоставление с образцом в do-блоках, чтобы извлечь необходимые значения: однако, фишка монад была также в том, чтобы избавиться от этого.

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

Проверка пароля

Рассмотрим обычную проблему из реальной жизни для ИТ-специалистов всего мира: составим программу запроса от пользователей паролей, которые трудно угадать. Типичная стратегия — заставить пользователя ввести пароль не короче минимальной длины, и по крайней мере одну букву, одну цифру (и с другими подобными раздражающими требованиями).

Функция получения пароля от пользователя на Haskell может выглядеть следующим образом:

getPassword :: IO (Maybe String)
getPassword = do s <- getLine
                 if isValid s then return $ Just s
                              else return Nothing

-- Тест правильности может быть произвольным по нашему желанию.
isValid :: String -> Bool
isValid s = length s >= 8 && any isAlpha s && any isNumber s && any isPunctuation s

В первую очередь, getPassword является IO-функцией, так как ей необходимо получить ввод от пользователя, ну и он не всегда будет возвращаться. Мы также используем Maybe, так как мы намерены возвращать Nothing в случае, если пароль не проходит условие isValid. Заметим, однако, что мы фактически не будем использовать Maybe здесь как монаду: do-блок находится в IO-монаде, и нам просто повезло получить return с Maybe-значением внутри.

Истинная мотивация для трансформеров монад состоит не только в том, чтобы было проще написать getPassword (что все же происходит), а, скорее, чтобы упростить все куски кода, в которых мы его используем. Наша программа получения пароля может быть продолжена так:

askPassword :: IO ()
askPassword = do putStrLn "Insert your new password:"
                 maybe_value <- getPassword
                 if isJust maybe_value 
                     then do putStrLn "Storing in database..."
                     -- ... other stuff, including 'else'

Нам нужна одна строка для создания maybe_value-переменной, а затем мы должны сделать некоторые дополнительные проверки, чтобы выяснить в порядке наш пароль или нет.

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

Простой монадный трансформер: MaybeT

Для упрощения функции getPassword и всего кода, который использует ее, мы определим монадный трансформер, который дает IO-монаде некоторые характеристики монады Maybe; мы будем называть его MaybeT, следуя соглашению, что трансформеры монад имеют «T», добавленное к названию монады, чьи характеристики они обеспечивают.

MaybeT является оберткой вокруг m (Maybe a), где m может быть любой монадой (в нашем примере, мы заинтересованы в IO):

newtype (Monad m) => MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

newtype — это просто более эффективная альтернатива обычным декларациям data на случаи, когда есть только один конструктор. Так, MaybeT является конструктором типа, параметризованного более m, с конструктором данных, который также называется MaybeT и удобной функцией доступа runMaybeT, с помощью которой мы можем получить доступ к базовому (внутреннему) представлению.

Весь смысл монадных трансформаторов в том, что они сами монады, и поэтому нам нужно сделать MaybeT m экземпляром класса Monad:

instance Monad m => Monad (MaybeT m) where
    return  = MaybeT . return . Just

return осуществляется с помощью Just, который вставляет (не указанное из-за частичного применения значение) в монаду Maybe, затем общий return, который в свою очередь вводит (полученное значение) в m (чем бы это ни было) и затем конструктор MaybeT. Также возможно (хотя менее приятно читается) написать return = MaybeT . return . return.

    x >>= f = MaybeT $ do maybe_value <- runMaybeT x
                          case maybe_value of
                               Nothing    -> return Nothing
                               Just value -> runMaybeT $ f value

Как и у всех монад, оператор связывания bind является сердцем трансформера, и самым важным фрагментом кода для понимания того, как он работает. Как всегда, полезно иметь в виду сигнатуру типов. Типом оператора связывания bind для монады MaybeT будет:

-- The signature of (>>=), specialized to MaybeT m
(>>=) :: MaybeT m a -> (a -> MaybeT m b) -> MaybeT m b

Теперь, давайте рассмотрим, что он делает, шаг за шагом, начиная с первой строки do-блока.

  • Во-первых, он использует функцию доступа runMaybeT, чтобы развернуть x в монадное вычисление m (Maybe a). Таким образом он разворачивает (разоблачает, выдает) do-блок находящийся в m.
  • И также в первой строке, (операция) <- извлекает значение Maybe a из развернутого вычисления.
  • Выражение case тестирует значение maybe_value:
    • если оно равно Nothing, возвращает Nothing внутрь монады m;
    • если оно является Just, применяется f к значению value внутри него. Так как функция f имеет MaybeT m b как тип результата, нам необходимо дополнительно (запускать) runMaybeT, чтобы поместить результат обратно внутрь монады m.
  • Наконец, do-блок в целом имеет тип m (Maybe b); так он обернут конструктором MaybeT. Это может выглядеть сложновато, но все же менее сложно, чем куча оберток и распаковок, реализация делает тоже самое, что и знакомый оператор bind монады Maybe:
-- (>>=) for the Maybe monad
    maybe_value >>= f = case maybe_value of
                            Nothing -> Nothing
                            Just value -> f value

Вы можете удивиться, почему мы используем конструктор MaybeT перед do-блоком, когда внутри него мы используем функцию доступа runMaybeT: однако, do-блок должен быть в монаде m, а не в MaybeT m, так как для последней мы еще не определили оператор связывания bind.

Информация

Связанные функции в определении return предполагают аналогию -- которую вы можете найти полезной или наоборот, путающей -- между комбинируемой монадой и сэндвичем. В этом примере Maybe, "базовая" монада, будет нижний слой; внутренняя монада m -- наполнением; и трансформатор MaybeT -- верхним слоем. Существует определенная опасность этой аналогии в том, что она может бы предложить три слоя монад в действии, в то время как на самом деле есть только два: внутренняя монада и комбинированная монада (нет связывания или возврата в базовую монаду, она появляется только как часть реализации трансформера). Лучший способ интерпретации аналогии -- это мыслить о трансформере и базовой монаде как двух частях одного и того же -- хлеб, который обертывает (оборачивается) внутреннюю монаду.

Технически, это все, что нам надо; однако, будет удобно сделать MaybeT воплощением еще некоторых других классов:

instance Monad m => MonadPlus (MaybeT m) where
    mzero     = MaybeT $ return Nothing
    mplus x y = MaybeT $ do maybe_value <- runMaybeT x
                            case maybe_value of
                                 Nothing    -> runMaybeT y
                                 Just _     -> return maybe_value

instance MonadTrans MaybeT where
    lift = MaybeT . (liftM Just)

Последний класс, MonadTrans, реализует (обеспечивает) функцию lift, которая очень полезна для того, чтобы взять функции из монады m и поднять (принести) их внутрь монады MaybeT m, так чтобы мы смогли использовать их в do-блоке внутри монады MaybeT m.

Применение к парольному примеру

Со всем сказанным выше, вот то, на что парольное управление будет похоже:

getValidPassword :: MaybeT IO String
getValidPassword = do s <- lift getLine
                      guard (isValid s) -- MonadPlus provides guard.
                      return s

askPassword :: MaybeT IO ()
askPassword = do lift $ putStrLn "Insert your new password:"
                 value <- getValidPassword
                 lift $ putStrLn "Storing in database..."

Этот код не прост, особенно в пользовательской функции askPassword. Более важно, что мы не должны вручную проверять, является ли результат Nothing или Just: оператор bind сделает это за нас.

Отметим как мы используем lift, чтобы поднять функции getLine и putStrLn вовнутрь монады MaybeT IO. Также, так как MaybeT IO является воплощением MonadPlus, проверка правильности пароля может быть осуществлена выражением guard, который вернет mzero (то есть IO Nothing) в случае плохого пароля.

И кстати, с помощью MonadPlus стало очень просто спрашивать у пользователя правильный пароль до бесконечности

askPassword :: MaybeT IO ()
askPassword = do lift $ putStrLn "Insert your new password:"
                 value <- msum $ repeat getValidPassword
                 lift $ putStrLn "Storing in database..."

Изобилие трансформеров

Модуль пакета transformers обеспечивает трансформеры для многих общих монад (MaybeT, например, может быть найдена в Шаблон:Haskell lib). Они определены в соответствии с их нетрансформерской версией; то есть, реализация в базовом та же самая, только с дополнительными обертками и развертками, необходимыми для ввинчивания в другую монаду.

Выберем произвольный пример, ReaderT Env IO String -- вычисление, которое вовлечет считываемое значение из некоторого окружения типа Env (семантика из Reader, базовой монады) и выполняет некоторое IO (действие) для того, чтобы получить значение типа String. Так как операторы bind и return отражают семантику базовой монады, do-блок типа ReaderT Env IO String будет с внешней стороны похож на do-блок монады Reader; главная разница будет в том, что IO-действия становятся тривиальными для встраивания при использовании функции lift.

Манипуляции с типами

Мы видели, что конструктор типа для MaybeT является оболочкой для значения типа Maybe во внутренней монаде, и поэтому соответствующая функция доступа runMaybeT дает нам значение типа m (Maybe a) — то есть, значение базовой монады возвращенную во внутреннюю монаду. Аналогичным образом, мы имеем

runListT :: ListT m a -> m [a]

и

runErrorT :: ErrorT e m a -> m (Either e a)

для трансформеров списков (list) и ошибок (error).

Все же, не все трансформеры связаны с базовой монадой таким образом. Монады Writer, Reader, State и Cont объединяет то, что в отличие от базовых монады в примерах выше, они не имеют ни нескольких конструкторов, ни конструкторов с несколькими аргументами. По этой причине, у них есть функции run..., которые действуют как простые развертыватели (unwrappers) аналогично соответственным run...T из версий трансформера. В приведенной ниже таблице показаны результаты типов функций run... и run...T в каждом конкретном случае, что может рассматриваться как типы обернутые базовой и трансформированной монадой соответственно.

Base Monad Transformer Original Type
(«wrapped» by base)
Combined Type
(«wrapped» by transformed)
Writer WriterT (a, w) m (a, w)
Reader ReaderT r -> a r -> m a
State StateT s -> (a, s) s -> m (a, s)
Cont ContT (a -> r) -> r (a -> m r) -> m r

Первое, на что нужно обратить внимание, это то, что базовые монады нигде не было видно в комбинированной типов. Это очень естественно, так как без конструкторы интересны (например, те для Может быть, или списки) нет никаких причин, чтобы сохранить базовый тип монады после разворачивания преобразованной монады. . Кроме того, в трех последних случаях у нас есть функция типа завернутые StateT , например, превращает государственные преобразования функций вида с -> (A, S) в государственно-преобразования функций вида с -> M (, с), так что только тип результата функции завернутые идет во внутренний монады. ReaderT аналогична, но не ContT : в связи с семантикой Cont (продолжение монады) результат обоих типов завернутые функции и ее функциональное Аргумент должен быть таким же, так что трансформатор ставит как во внутреннюю монады. Что эти примеры показывают, что в целом нет никакой волшебной формулы для создания трансформатора версия монады-формы каждого трансформера, зависит от того, что имеет смысл в контексте его не-трансформатор типа.

Подъем

Функция lift, которую мы впервые представили во введении, очень важна для повседневного использования трансформеров; и поэтому мы посвятим несколько слов ей.

liftM

Начнем с того, что строго говоря, она не из темы монадных трансформеров. liftM — черезвычайно полезная функция в стандартной библиотеке со следующей сигнатурой:

liftM :: Monad m => (a1 -> r) -> m a1 -> m r

liftM применяет функцию (a1 -> r) к значению в рамках (внутри) монады m. Если вы предпочитаете бесточечную запись, она может превратить обычную функцию в такую, которая действует внутри m — и это как раз то, что подразумевается под поднятием в монаду.

Давайте посомтрим, как liftM используется. Следующие фрагменты обозначают одно и то же.

do notation liftM liftM as an operator
do x <- monadicValue
   return (f x)
liftM f monadicValue
f `liftM` monadicValue

Третий пример, в котором мы используем liftM как оператор, предлагает интересную точку зрения на liftM: это просто монадическая версия ($)!

non monadic monadic
f $ value
f `liftM` monadicValue
Упражнения
  1. Напишите реализацию для liftM.

lift

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

С liftM мы увидели, что сущность поднятия — перефразируя документацию — в продвижении чего-то в монаду. Функция lift, доступная для всех монадных трансформеров, выполняет разный тип поднятия: она продвигает вычисление из внутренней монады в комбинированную монаду. Функция lift определена как единственный метод класса MonadTrans в Шаблон:Haskell lib.

class MonadTrans t where
    lift :: (Monad m) => m a -> t m a

Имеется вариант lift, специфичный для IO и называемый liftIO, который является единственным методом класса MonadIO в Шаблон:Haskell lib.

class (Monad m) => MonadIO m where
   liftIO :: IO a -> m a

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

Реализация lift

Реализация lift как правило довольно прямолинейна (проста). Рассмотрим трансформер MaybeT:

instance MonadTrans MaybeT where
    lift m = MaybeT (m >>= return . Just)

Мы начинаем с монадическим значением внутренней монады — средним слоем, если вы предпочитаете аналогию с сэндвичем. Используя оператор bind и конструктор типа для базовой монады, мы плавно сдвигаем (скатываем, намазываем ??) нижний слой (базовую монаду) под средний слой. В конце, мы помещаем верхний срез нашего сэндвича с помощью конструктора MaybeT. Таким образом, используя функцию lift, мы трансформировали нижний кусок начинки сэндвича в подлинно трехслойный монадический сэндвич. Отметим, что как в реализации класса Monad, и оператор bind, и общий (основной) оператор return работают в границах внутренней монады.

Упражнения
  1. Почему функция lift была определена отдельно для каждой монады, тогда как функция liftM может быть определена универсальным способом?
  2. Реализуйте функцию lift для трансформера ListT.
  3. Как бы подняли обычную функцию в монадный трансформер? Подсказка: очень легко.

Реализация трансформеров

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

Трансформер List

Также как с трансформером Maybe, мы начнем с создания конструктора типа, который принимает один аргумент:

newtype ListT m a = ListT { runListT :: m [a] }

Реализация монады ListT m поразительно похожа на свою «кузину», монаду списков. Мы делаем те же самые операции внутри внутренней монады m, пакуем и распаковываем монадический сэндвич.

List ListT
instance Monad [] where
    xs >>= f =

        let yss = map f xs
        in concat yss
instance (Monad m) => Monad (ListT m) where
    tm >>= f = ListT $ runListT tm
                 >>= \xs -> mapM (runListT . f) xs
                   >>= \yss -> return (concat yss)
Упражнения
  1. Препарируйте оператор bind для монады (ListT m). К примеру, почему у нас уже есть mapM and return?
  2. Сейчас когда вы увидели два простых монадных трансформера, напишите монадный трансформер IdentityT, который был бы трансформирующей кузиной монады Identity.
  3. Будет ли IdentityT SomeMonad эквивалентна SomeMonadT Identity для заданной монады и ее кузины-трансформера?

Трансформер State

В прошлый раз мы пристально рассмотрели реализацию двух простых монадных трансформеров, MaybeT и ListT, совершив обходной путь, чтобы обсудить подъем из (простой) монады в ее вариант-трансформер. Теперь, соединим две идеи вместе, внимательно рассмотрев реализацию одного из наиболее интересных трансформеров в стандартной библиотеке, StateT. Изучение этого трансформера сотворит озарение в понимании механизма трансформеров, которое вы сможете призвать впоследствии, когда будуте использовать монадные трансформеры в вашем коде. Прежде чем продолжить, вам может быть понадобиться освежить в памяти или просмотреть State monad.

Как и монада State могла быть построена определением newtype State s a = State { runState :: (s -> (a,s)) }[1] Трансформер StateT создан определением

newtype StateT s m a = StateT { runStateT :: (s -> m (a,s)) }

State s является воплощением как класса Monad, так и класса MonadState s (который обеспечивает get и put), так что StateT s m должна быть членом классов Monad и MonadState s. Более того, если m является воплощением MonadPlus, StateT s m также должна быть членом MonadPlus.

Определим StateT s m как воплощение Monad:

State StateT
newtype State s a =
  State { runState :: (s -> (a,s)) }

instance Monad (State s) where
  return a        = State $ \s -> (a,s)
  (State x) >>= f = State $ \s ->
    let (v,s') = x s
    in runState (f v) s'
newtype StateT s m a =
  StateT { runStateT :: (s -> m (a,s)) }

instance (Monad m) => Monad (StateT s m) where
  return a         = StateT $ \s -> return (a,s)
  (StateT x) >>= f = StateT $ \s -> do
    (v,s') <- x s          -- get new value and state
    runStateT (f v) s'     -- pass them to f

Наше определение return использует функцию return внутренней монады, и оператор связывания использует do-блок, чтобы выполнить вычисление во внутренней монаде.

Мы также хотим декларировать все комбинированные монады, которые используют трансформер StateT, как воплощения класса MonadState, так что мы дадим определения get и put:

instance (Monad m) => MonadState s (StateT s m) where
  get   = StateT $ \s -> return (s,s)
  put s = StateT $ \_ -> return ((),s)

Наконец, мы хотим декларировать все комбинированные монады, в которых используется StateT с воплощением MonadPlus, как воплощения класса MonadPlus:

instance (MonadPlus m) => MonadPlus (StateT s m) where
  mzero = StateT $ \s -> mzero
  (StateT x1) `mplus` (StateT x2) = StateT $ \s -> (x1 s) `mplus` (x2 s)

Последним шагом сделаем наш монадный трансформер полностью интегрированным с классом монад Haskell’а — для этого сделаем StateT s воплощением класса MonadTrans, обеспечив функцию lift:

instance MonadTrans (StateT s) where
  lift c = StateT $ \s -> c >>= (\x -> return (x,s))

Функция lift создает функцию, изменяющую состояние, StateT, которая связывает вычисление во внутренней монаде с функцией, пакующей результат со входным состоянием. Результат в том, если для воплощения мы применяем StateT к монаде the List, функция, которая возвращает список (то есть, вычисление в монаде List) может быть поднято вовнутрь StateT s [], где оно станет функцией, которая возвращает StateT (s -> [(a,s)]). Таким образом, поднятое вычисление производит множественные пары (значение, состояние) из его внутреннего состояния. Эффект выразится в «разветвлении» вычисления в StateT, создавая разные ветви для разных значений в списке, возращенном поднятой функцией. Разумеется, применяя StateT к разным монадам, получим разную семантику функции lift.

Благодарности

Этот модуль использует ряд отрывков из All About Monads, с разрешения автора Jeff Newbern.

Примечания

  1. В версии пакета mtl ранее 2.0.0.0, так и было построено. В настоящее время, однако, State s реализована как синоним типа для StateT s Identity.


Шаблон:Haskell/NotesSection

Шаблон:Haskell navigation