Haskell/Monad transformers
К этому моменту вы должны были предварительно уяснить понятие монады, а также то, что различные монады используются еще для: IO для «нечистых» функций, Maybe для значений, которые могут быть или нет, и так далее.
С помощью монад, обеспечивающих такую полезную функциональность общего назначения, очень естественно, что порой мы хотели бы использовать возможности нескольких монад сразу — например, функция, которая использует и IO, и обработку исключений Maybe. Конечно, мы можем использовать такой тип как IO (Maybe a), но это заставляет нас делать сопоставление с образцом в do-блоках, чтобы извлечь необходимые значения: однако, фишка монад была также в том, чтобы избавиться от этого.
Итак, рассмотрим монадные трансформеры: специальные типы, которые позволяют нам комбинировать две монады в одной, но разделяющие поведение обеих. Начнем с примера, чтобы проиллюстрировать, почему трансформеры полезны и показать, как они работают.
Проверка пароля
Рассмотрим обычную проблему из реальной жизни ИТ-специалистов всего мира: составим программу, запрашивающую у пользователей пароли, которые трудно угадать. Типичная стратегия — заставить пользователя ввести пароль не короче минимальной длины, содержащий по крайней мере одну букву и одну цифру (и отвечающий прочим раздражающим требованиям).
Функция получения пароля от пользователя на Haskell может выглядеть следующим образом:
getPassphrase :: IO (Maybe String)
getPassphrase = 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
Прежде всего getPassphrase это действие ввода/вывода, возвращающее монаду IO, потому что она работает с пользовательским вводом. Кроме того мы используем Maybe, потому что хотим возвращать Nothing, если пароль не проходит проверку isValid. Заметим, однако, что использовать Maybe как монаду мы здесь по сути не будем: do-блок находится в монаде IO, и нам просто повезло получить return с Maybe-значением внутри.
Но трансформеры монад упрощают нам код не только самой функции getPassphrase, но и тех мест, где она используется. Дальше в нашей программе сбора паролей может быть вот что:
askPassphrase :: IO ()
askPassphrase = do putStrLn "Insert your new passphrase:"
maybe_value <- getPassphrase
case maybe_value of
Just value -> do putStrLn "Storing in database..." -- do stuff
Nothing -> putStrLn "Passphrase invalid."
Мы одной строчкой получаем переменную maybe_value-переменной, а дальше проверяем, в порядке наш пароль или нет.
С монадными трансформерами мы сможем извлечь пароль на одном дыхании, безо всякого сопоставления с образцом или ещё какой-то бюрократии типа isJust. Выгода для нашего простого примера может показаться небольшой, но чем сложнее будет код, тем эта выгода будет больше.
Простой монадный трансформер: MaybeT
Для упрощения функции getPassphrase и использующего её кода мы определим монадный трансформер, который даёт монаде IO некоторые характеристики монады Maybe. Мы будем называть его MaybeT, следуя соглашению, что имя монадного трансформера составляется из названия монады, чьи характеристики он обеспечивает, и буквы «T».
MaybeT это обертка вокруг m (Maybe a), где m может быть любой монадой (в нашем примере это IO):
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
Здесь определён конструктор типа MaybeT, параметризуемый типом m, с конструктором данных, который тоже называется MaybeT, и вспомогательной функцией runMaybeT, обеспечивающей доступ к содержимому типа.
Идея монадного трансформера — в создании монады, ведущей себя как другая монада, так что MaybeT m нужно сделать экземпляром класса Monad:
instance Monad m => Monad (MaybeT m) where
return = MaybeT . return . Just
Функция return оборачивает переданное ей значение (имя которого мы не указали, потому что используем частичное применение) в монаду Maybe с помощью Just, а потом в монаду m (чем бы она ни была) с помощью return; результат передаётся конструктору MaybeT.
Можно было бы написать return = MaybeT . return . return, но понять такой код было бы ещё сложнее.
-- Тип функции (>>=), определённой на MaybeT m:
-- (>>=) :: MaybeT m a -> (a -> MaybeT m b) -> MaybeT m b
x >>= f = MaybeT $ do maybe_value <- runMaybeT x
case maybe_value of
Nothing -> return Nothing
Just value -> runMaybeT $ f value
Как это обычно бывает с монадами, для понимания работы MaybeT важнее всего разобраться, как работает оператор связывания >>=. Рассмотрим, что он делает, шаг за шагом, начиная с первой строки блока do.
- Первым делом мы извлекаем из
xзначение типаm (Maybe a)с помощью функцииrunMaybeT; соответственно, весь блокdoбудет работать с монадойm. - В той же строке мы извлекаем значение
Maybe aиз монадыmи помещаем его вmaybe_valueс помощью оператора<-. - Выражение
caseпроверяет значениеmaybe_valueи:- …если это
Nothing, возвращаетNothing, завёрнутый вmс помощьюreturn; - …eсли это
Just value, применяет кvalueфункциюf; так как функцияfуже возвращает значение типаMaybeT m b, нам нужно извлечь из него значение типаm bс помощьюrunMaybeT— ведь блокdoработает с монадойm, и из всех его веток мы должны вернуть именно её.
- …если это
- А в конце мы завернём обратно полученное внутри блока
doзначение типаm (Maybe b)с помощью конструктораMaybeT.
Всё это может смотреться сложновато, но за вычетом всех этих разворачиваний и заворачиваний по сути здесь происходит то же, что в хорошо знакомом нам >>= для Maybe:
-- (>>=) для монады Maybe
maybe_value >>= f = case maybe_value of
Nothing -> Nothing
Just value -> f value
Вас может удивить, что мы разворачиваем наш тип с помощью runMaybeT внутри блока do только затем, чтобы сразу же завернуть его результат обратно с помощью MaybeT; но дело в том, что в этом месте мы можем воспользоваться синтаксисом do только для монады m, потому что блок do использует оператор >>=, а для MaybeT он, понятно, ещё не определён — как раз его определением мы здесь и заняты.
Как обычно, для нашего типа нужно породить экземпляры классов-предков класса Monad — Applicative и Functor:
instance Monad m => Applicative (MaybeT m) where
pure = return
(<*>) = ap
instance Monad m => Functor (MaybeT m) where
fmap = liftM
Кроме того, будет удобно, если MaybeT m будет экземпляром ещё пары классов типов:
instance Monad m => Alternative (MaybeT m) where
empty = MaybeT $ return Nothing
x <|> y = MaybeT $ do maybe_value <- runMaybeT x
case maybe_value of
Nothing -> runMaybeT y
Just _ -> return maybe_value
instance Monad m => MonadPlus (MaybeT m) where
mzero = empty
mplus = (<|>)
instance MonadTrans MaybeT where
lift = MaybeT . (liftM Just)
В классе MonadTrans определяется функция lift, так что теперь можно просто брать функции, работающие с монадой m, поднимать их в монаду MaybeT m и работать с ними в блоках do как с функциями для монады MaybeT m. Что же до Alternative и MonadPlus, то раз уж для Maybe определены экземпляры этих классов, логично определить их и для MaybeT m тоже.
Упрощаем проверку паролей
Монадный трансформер MaybeT позволяет нам переписать определённую выше функцию проверки паролей так:
getPassphrase :: MaybeT IO String
getPassphrase = do s <- lift getLine
guard (isValid s) -- функция guard определена для класса Alternative.
return s
askPassphrase :: MaybeT IO ()
askPassphrase = do lift $ putStrLn "Insert your new passphrase:"
value <- getPassphrase
lift $ putStrLn "Storing in database..."
Код стал заметно проще, особенно в функции askPassphrase. Но главное — теперь не надо вручную проверять, равен результат Nothing или Just: оператор >>= делает это за нас.
Обратите внимание, как мы используем lift, чтобы заставить функции getLine и putStrLn работать с монадой MaybeT IO. А так как для MaybeT IO определён экземпляр класса Alternative, о проверке пароля позаботится выражение guard, которое вернёт empty (т.е. IO Nothing) для слишком слабых паролей.
Заодно благодаря MonadPlus теперь очень просто до бесконечности требовать у пользователя подходящий пароль:
askPassphrase :: MaybeT IO ()
askPassphrase = do lift $ putStrLn "Insert your new passphrase:"
value <- msum $ repeat getPassphrase
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
|
| Упражнения |
|---|
|
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 работают в границах внутренней монады.
| Упражнения |
|---|
|
Реализация трансформеров
Для того, чтобы развить лучшее понимание работы трансформеров, мы обсудим две реализации в стандартных библиотеках.
Трансформер 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)
|
| Упражнения |
|---|
|
Трансформер 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.
Примечания
- ↑ В версии пакета mtl ранее 2.0.0.0, так и было построено. В настоящее время, однако,
State sреализована как синоним типа дляStateT s Identity.
Шаблон:Haskell/NotesSection
Шаблон:Haskell navigation