Почему монады-трансформаторы отличаются друг от друга укладыванием монад?

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

Один случай, как представляется, является StateT on List: объединение монад не дает вам нужного типа, и если вы получите правильный тип через стек монад вроде Bar (где Bar a = (Reader r (List (Writer) w (Идентификация a))), она не делает правильные вещи.

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

Чтобы сделать этот вопрос немного более сосредоточенным:

  • Каков фактический пример монады без соответствующего трансформатора (это поможет проиллюстрировать, что могут сделать трансформаторы, которые не могут просто укладывать монады).
  • Являются ли StateT и ContT единственными трансформаторами, которые дают тип, не эквивалентный их составу с m, для лежащей в основе монады m (независимо от того, какой порядок они составлены.)

(Меня не интересуют конкретные детали реализации в отношении разных вариантов библиотек, а скорее общий (и, вероятно, независимый от Haskell) вопрос о том, какие монады-трансформаторы/морфизмы добавляют в качестве альтернативы объединению эффектов, складывая кучу конструкторы монадического типа.)

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

Спасибо,

Реубен

Ответы

Ответ 1

Чтобы ответить на вопрос о разнице между Writer w (Maybe a) vs MaybeT (Writer w) a, давайте начнем с определения:

newtype WriterT w m a = WriterT { runWriterT :: m (a, w) }
type Writer w = WriterT w Identity

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

Используя ~~, чтобы означать "структурно подобный", мы имеем:

Writer w (Maybe a)  == WriterT w Identity (Maybe a)
                    ~~ Identity (Maybe a, w)
                    ~~ (Maybe a, w)

MaybeT (Writer w) a ~~ (Writer w) (Maybe a)
                    == Writer w (Maybe a)
                    ... same derivation as above ...
                    ~~ (Maybe a, w)

Итак, в некотором смысле вы правы - структурно и Writer w (Maybe a), и MaybeT (Writer w) a одинаковы - оба являются по существу просто парой значения Maybe и w.

Различие заключается в том, как мы рассматриваем их как монадические значения. Функции класса return и >>= выполняют разные вещи в зависимости от на которых монада они являются частью.

Рассмотрим пару (Just 3, []::[String]). Используя ассоциацию мы здесь вывели здесь, как эта пара будет выражена в обеих монадах:

three_W :: Writer String (Maybe Int)
three_W = return (Just 3)

three_M :: MaybeT (Writer String) Int
three_M = return 3

И вот как мы построим пару (Nothing, []):

nutin_W :: Writer String (Maybe Int)
nutin_W = return Nothing

nutin_M :: MaybeT (Writer String) Int
nutin_M = MaybeT (return Nothing)   -- could also use mzero

Теперь рассмотрим эту функцию для пар:

add1 :: (Maybe Int, String) -> (Maybe Int, String)
add1 (Nothing, w) = (Nothing w)
add1 (Just x, w)  = (Just (x+1), w)

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

add1_W :: Writer String (Maybe Int) -> Writer String (Maybe Int)
add1_W e = do x <- e
             case x of
               Nothing -> return Nothing
               Just y  -> return (Just (y+1))

add1_M :: MaybeT (Writer String) Int -> MaybeT (Writer String) Int
add1_M e = do x <- e; return (e+1)
  -- also could use: fmap (+1) e

В общем, вы увидите, что код в монаде MaybeT более краткий.

Кроме того, семантически две монады очень разные...

MaybeT (Writer w) a - это действие Writer, которое может выйти из строя, а сбой - автоматически обрабатывается для вас. Writer w (Maybe a) - это просто писатель действие, которое возвращает Maybe. Ничего особенного не происходит, если это значение Maybe оказывается ничто. Это проиллюстрировано в функции add1_W, где нам пришлось выполнить анализ случая на x.

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

square x = do tell ("computing the square of " ++ show x)
              return (x*x)

может использоваться без изменений в любом стеке монады, который имеет строку Writer, например:

WriterT String IO
ReaderT (WriterT String Maybe)
MaybeT (Writer String)
StateT (WriterT String (ReaderT Char IO))
...

Но возвращаемое значение square не устанавливает проверку с Writer String (Maybe Int), потому что square не возвращает Maybe.

Когда вы вводите код Writer String (Maybe Int), вы явно указываете код структура монады делает его менее общим. Это определение add1_W:

add1_W e = do x <- e 
              return $ do 
                y <- x 
                return $ y + 1

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

Ответ 2

Каков фактический пример монады без соответствующего трансформатора (это поможет проиллюстрировать, какие трансформаторы могут делать, что просто укладки монад не могут).

IO и ST являются каноническими примерами здесь.

Являются ли StateT и ContT единственными трансформаторами, которые дают тип, не эквивалентный их составу с m, для основной монады m (независимо от того, в каком порядке они составлены.)

Нет, ListT m a не (изоморфно) [m a]:

newtype ListT m a =
  ListT { unListT :: m (Maybe (a, ListT m a)) }