Попытка понять типы, производимые монадными трансформаторами

docs для Control.Monad.Trans.Error предоставляют этот пример объединения двух монад:

type ErrorWithIO e a = ErrorT e IO a
==> ErrorT (IO (Either e a))

Я нахожу этот контрапункт: даже если ErrorT предположительно обертывает IO, похоже, что информация об ошибке была введена в тип результата действия IO. Я ожидал, что это будет

==> ErrorT (Either e (IO a))

на основе обычного значения слова "wrap".

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

type MyError e = ErrorT e Identity  -- (see footnote)
type StateWithError s e a = StateT s (MyError e) a
==> StateT (s -> ErrorT (Either e (a, s)))

Тип состояния s был введен в сторону Either Right, но весь Either также был обернут в функцию.

Чтобы сделать вещи еще более запутанными, если монады объединены наоборот:

type ErrorWithState e s a = ErrorT e (State s) a
==> ErrorT (StateT (s -> (Either e a, s)))

"внешний" по-прежнему является функцией; он не создает нечто вроде Either e (s -> (a, s)), где функция состояния вложена в тип ошибки.

Я уверен, что есть какая-то основная логическая последовательность для всего этого, но я не совсем это вижу. Следовательно, мне трудно думать о том, что значит объединить одну монаду с другой, даже когда я не понимаю, что каждая монада означает индивидуально.

Может кто-нибудь просветить меня?


( Сноска: Я составляю ErrorT с Identity, так что StateWithError и ErrorWithState совместимы друг с другом для иллюстративных целей. Обычно я просто использовал StateWithError s e a = StateT s (Either e) a и отказаться от слоя ErrorT.

Ответы

Ответ 1

Я нахожу этот контрапункт: хотя ErrorT предположительно обматывает IO, похоже, что информация об ошибке была введена в тип результата действия IO.

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

Чтобы проиллюстрировать, структура функтора для State s и Maybe с расширенными определениями будет выглядеть так:

newtype StateMaybe s a = StateMaybe (s -> (Maybe a, s))    -- == State s (Maybe a)
newtype MaybeState s a = MaybeState (Maybe (s -> (a, s)))  -- == Maybe (State s a)

Обратите внимание, что в первом случае State ведет себя нормально, а Nothing не влияет на значение состояния; во втором случае мы либо имеем обычную функцию State, либо ничего вообще. В любом случае характерное поведение двух монадов фактически не сочетается. Это не должно удивлять, поскольку, в конце концов, это то же самое, что и вы, просто имея значения, используя одну монаду как обычные значения, используемые в другом.

Сравните это с StateT s Maybe:

newtype StateTMaybe s a = StateTMaybe (s -> Maybe (a, s))

В этом случае они сплетены вместе; все происходит обычным образом для State, если мы не нажмем a Nothing, и в этом случае вычисление будет прервано. Это принципиально отличается от вышеуказанных случаев, поэтому монады-трансформаторы даже существуют в первую очередь - составление их наивно не требует какой-либо специальной техники, потому что они работают независимо друг от друга.


Что касается определения того, какой из них находится на "вне", это может помочь подумать о "внешнем" трансформаторе как о том, чье поведение принимает "приоритет", в некотором смысле, когда имеет дело со значениями в монады, в то время как "внутренняя" монада видит бизнес как обычно. Обратите внимание, что именно поэтому IO всегда является самым внутренним - он не позволяет чему-либо еще вставать в своем бизнесе, тогда как гипотетический трансформатор IOT будет вынужден разрешить завернутой монаде тянуть все виды махинаций, например дублировать или отбрасывание токена RealWorld.

  • StateT и ReaderT обе кладут "внутреннюю" монаду вокруг результата функции; вам нужно предоставить значение состояния или среду, прежде чем попасть в преобразованную монаду.

  • MaybeT и ErrorT оба скользят внутрь внутри преобразованной монады, гарантируя, что она может вести себя обычным образом, за исключением того, что значение может отсутствовать.

  • Writer является полностью пассивным и просто присоединяется к значениям в монаде, поскольку он вообще не влияет на поведение.

  • ContT хранит вещи сам по себе, полностью отделяя преобразованную монаду, только обертывая тип результата.

Это немного волнообразные, но эхо-монадные трансформаторы, вроде бы, разборчивы и сбивают с толку, увы. Я не знаю, есть ли какое-то чистое теоретическое обоснование для конкретного выбора, кроме того, что они работают, и делать то, что вам обычно требуется, чтобы комбинация (а не состав) двух монадов.

Следовательно, мне трудно думать о том, что значит объединить одну монаду с другой, даже когда я не понимаю, что каждая монада означает индивидуально.

Да, это звучит как о том, чего ожидать, я боюсь.

Ответ 2

Подумайте, что произойдет, если ErrorT будет определен так, как вы его себе представляете. Как бы вы кодировали операцию ввода-вывода, что не удалось? С Either e (IO a) вы не можете дать значение Left, когда действие завершится неудачно, потому что когда вы достигли действия, оно уже очистило его значение Right - иначе это не было бы действием.

С IO (Either e a) однако это не так. Теперь все это действие IO и может вернуть значение Left для указания ошибки. Как отмечали другие, не думайте о трансформаторах монады как обертках. Скорее думайте о них как о функциях. Они берут монаду и превращают ее в другую монаду. Они преобразуют монады.