Haskell/Monad transformers

Материал из Викиучебника — открытых книг для открытого мира
Перейти к навигации Перейти к поиску

К этому моменту вы должны были предварительно уяснить понятие монады, а также то, что различные монады используются еще для: 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