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

540 байт добавлено ,  1 год назад
м
<source> -> <syntaxhighlight> (phab:T237267)
(→‎Реализация lift: орфография)
м (<source> -> <syntaxhighlight> (phab:T237267))
 
Функция получения пароля от пользователя на Haskell может выглядеть следующим образом:
<sourcesyntaxhighlight lang="haskell">
getPassword :: IO (Maybe String)
getPassword = do s <- getLine
isValid :: String -> Bool
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> (что все же происходит), а, скорее, чтобы упростить все куски кода, в которых мы его используем. Наша программа получения пароля может быть продолжена так:
<sourcesyntaxhighlight lang="haskell">
askPassword :: IO ()
askPassword = do putStrLn "Insert your new password:"
then do putStrLn "Storing in database..."
-- ... other stuff, including 'else'
</syntaxhighlight>
</source>
Нам нужна одна строка для создания <code>maybe_value</code>-переменной, а затем мы должны сделать некоторые дополнительные проверки, чтобы выяснить в порядке наш пароль или нет.
 
 
<code>MaybeT</code> является оберткой вокруг <code>m (Maybe a)</code>, где <code>m</code> может быть любой монадой (в нашем примере, мы заинтересованы в <code>IO</code>):
<sourcesyntaxhighlight lang="haskell">
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>MaybeT m</code> экземпляром класса <code>Monad</code>:
<sourcesyntaxhighlight lang="haskell">
instance Monad m => Monad (MaybeT m) where
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 = MaybeT . return . return</code>.
 
<sourcesyntaxhighlight lang="haskell">
x >>= f = MaybeT $ do maybe_value <- runMaybeT x
case maybe_value of
Nothing -> return Nothing
Just value -> runMaybeT $ f value
</syntaxhighlight>
</source>
Как и у всех монад, оператор связывания <code>bind</code> является сердцем трансформера, и самым важным фрагментом кода для понимания того, как он работает. Как всегда, полезно иметь в виду сигнатуру типов. Типом оператора связывания <code>bind</code> для монады <code>MaybeT</code> будет:
 
<sourcesyntaxhighlight lang="haskell">
-- The signature of (>>=), specialized to MaybeT m
(>>=) :: MaybeT m a -> (a -> MaybeT m b) -> MaybeT m b
</syntaxhighlight>
</source>
Теперь, давайте рассмотрим, что он делает, шаг за шагом, начиная с первой строки <code>do</code>-блока.
 
* Наконец, <code>do</code>-блок в целом имеет тип <code>m (Maybe b)</code>; так он обернут конструктором <code>MaybeT</code>. Это может выглядеть сложновато, но все же менее сложно, чем куча оберток и распаковок, реализация делает тоже самое, что и знакомый оператор <code>bind</code> монады <code>Maybe</code>:
 
<sourcesyntaxhighlight lang="haskell">
-- (>>=) for the Maybe monad
maybe_value >>= f = case maybe_value of
Nothing -> Nothing
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> воплощением еще некоторых других классов:
<sourcesyntaxhighlight lang="haskell">
instance Monad m => MonadPlus (MaybeT m) where
mzero = MaybeT $ return Nothing
instance MonadTrans MaybeT where
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>.
=== Применение к парольному примеру ===
Со всем сказанным выше, вот то, на что парольное управление будет похоже:
<sourcesyntaxhighlight lang="haskell">
getValidPassword :: MaybeT IO String
getValidPassword = do s <- lift getLine
value <- getValidPassword
lift $ putStrLn "Storing in database..."
</syntaxhighlight>
</source>
Этот код не прост, особенно в пользовательской функции <code>askPassword</code>. Более важно, что мы не должны вручную проверять, является ли результат <code>Nothing</code> или <code>Just</code>: оператор <code>bind</code> сделает это за нас.
 
 
И кстати, с помощью <code>MonadPlus</code> стало очень просто спрашивать у пользователя правильный пароль ''до бесконечности''
<sourcesyntaxhighlight lang="haskell">
askPassword :: MaybeT IO ()
askPassword = do lift $ putStrLn "Insert your new password:"
value <- msum $ repeat getValidPassword
lift $ putStrLn "Storing in database..."
</syntaxhighlight>
</source>
 
== Изобилие трансформеров ==
=== Манипуляции с типами ===
Мы видели, что конструктор типа для <code>MaybeT</code> является оболочкой для значения типа <code>Maybe</code> во внутренней монаде, и поэтому соответствующая функция доступа <code>runMaybeT</code> дает нам значение типа <code>m&nbsp;(Maybe a)</code> — то есть, значение базовой монады возвращенную во внутреннюю монаду. Аналогичным образом, мы имеем
<sourcesyntaxhighlight lang = "haskell">
runListT :: ListT m a -> m [a]
</syntaxhighlight>
</source>
и
<sourcesyntaxhighlight lang = "haskell">
runErrorT :: ErrorT e m a -> m (Either e a)
</syntaxhighlight>
</source>
для трансформеров списков (list) и ошибок (error).
 
Начнем с того, что строго говоря, она не из темы монадных трансформеров. <code>liftM</code> — черезвычайно полезная функция в стандартной библиотеке со следующей сигнатурой:
 
<sourcesyntaxhighlight lang="haskell">
liftM :: Monad m => (a1 -> r) -> m a1 -> m r
</syntaxhighlight>
</source>
 
<code>liftM</code> применяет функцию <code>(a1 -> r)</code> к значению в рамках (внутри) монады <code>m</code>. Если вы предпочитаете бесточечную запись, она может превратить обычную функцию в такую, которая действует внутри <code>m</code> — и ''это'' как раз то, что подразумевается под поднятием в монаду.
|-
|valign="top"|
<sourcesyntaxhighlight lang="haskell">
do x <- monadicValue
return (f x)
</syntaxhighlight>
</source>
|valign="top"|
<sourcesyntaxhighlight lang="haskell">
liftM f monadicValue
</syntaxhighlight>
</source>
|valign="top"|
<sourcesyntaxhighlight lang="haskell">
f `liftM` monadicValue
</syntaxhighlight>
</source>
|}
 
|-
||
<sourcesyntaxhighlight lang="haskell">
f $ value
</syntaxhighlight>
</source>
||
<sourcesyntaxhighlight lang="haskell">
f `liftM` monadicValue
</syntaxhighlight>
</source>
|}
 
С <code>liftM</code> мы увидели, что сущность поднятия — перефразируя документацию — в продвижении чего-то в монаду. Функция <code>lift</code>, доступная для всех монадных трансформеров, выполняет разный тип поднятия: она продвигает вычисление из внутренней монады в комбинированную монаду. Функция <code>lift</code> определена как единственный метод класса <code>MonadTrans</code> в {{Haskell lib|p=transformers|v=latest|Control|Monad|Trans|Class}}.
 
<sourcesyntaxhighlight lang="haskell">
class MonadTrans t where
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}}.
 
<sourcesyntaxhighlight lang="haskell">
class (Monad m) => MonadIO m where
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>lift</code> как правило довольно прямолинейна (проста). Рассмотрим трансформер <code>MaybeT</code>:
 
<sourcesyntaxhighlight lang="haskell">
instance MonadTrans MaybeT where
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> работают в границах внутренней монады.
=== Трансформер List ===
Также как с трансформером <code>Maybe</code>, мы начнем с создания конструктора типа, который принимает один аргумент:
<sourcesyntaxhighlight lang="haskell">
newtype ListT m a = ListT { runListT :: m [a] }
</syntaxhighlight>
</source>
 
Реализация монады <code>ListT m</code> поразительно похожа на свою «кузину», монаду списков. Мы делаем те же самые операции
|-
|valign="top"|
<sourcesyntaxhighlight lang="haskell">
instance Monad [] where
xs >>= f =
let yss = map f xs
in concat yss
</syntaxhighlight>
</source>
|valign="top"|
<sourcesyntaxhighlight lang="haskell">
instance (Monad m) => Monad (ListT m) where
tm >>= f = ListT $ runListT tm
>>= \xs -> mapM (runListT . f) xs
>>= \yss -> return (concat yss)
</syntaxhighlight>
</source>
|}
 
 
Как и монада <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> создан определением
<sourcesyntaxhighlight lang="haskell">
newtype StateT s m a = StateT { runStateT :: (s -> m (a,s)) }</sourcesyntaxhighlight>
 
<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>.
!|StateT
|-valign="top"
||<sourcesyntaxhighlight lang="haskell">
newtype State s a =
State { runState :: (s -> (a,s)) }
let (v,s') = x s
in runState (f v) s'
</syntaxhighlight>
</source>
||<sourcesyntaxhighlight lang="haskell">
newtype StateT s m a =
StateT { runStateT :: (s -> m (a,s)) }
(v,s') <- x s -- get new value and state
runStateT (f v) s' -- pass them to f
</syntaxhighlight>
</source>
|}
 
класса <code>MonadState</code>, так что мы дадим определения <code>get</code> и <code>put</code>:
 
<sourcesyntaxhighlight lang="haskell">
instance (Monad m) => MonadState s (StateT s m) where
get = StateT $ \s -> return (s,s)
put s = StateT $ \_ -> return ((),s)
</syntaxhighlight>
</source>
 
Наконец, мы хотим декларировать все комбинированные монады, в которых используется <code>StateT</code> с воплощением <code>MonadPlus</code>, как воплощения класса <code>MonadPlus</code>:
<sourcesyntaxhighlight lang="haskell">
instance (MonadPlus m) => MonadPlus (StateT s m) where
mzero = StateT $ \s -> mzero
(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>:
<sourcesyntaxhighlight lang="haskell">
instance MonadTrans (StateT s) where
lift c = StateT $ \s -> c >>= (\x -> return (x,s))
</syntaxhighlight>
</source>
 
Функция <code>lift</code> создает функцию, изменяющую состояние, <code>StateT</code>, которая связывает вычисление во внутренней монаде с функцией, пакующей результат со входным состоянием. Результат в том, если для воплощения мы применяем
583

правки