Haskell/Monad transformers

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

Шаблон:Haskell minitoc

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

Шаблон:Exercises

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 работают в границах внутренней монады.

Шаблон:Exercises

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

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

Трансформер 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)

Шаблон:Exercises

Трансформер 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