Попытка понять типы, произведенные монадными трансформаторами - PullRequest
15 голосов
/ 17 августа 2011

Документы для 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))

, основанное на обычном значении слова «обертка».

Чтобы сделать вещи более запутанными, 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 слоя.

Ответы [ 2 ]

18 голосов
/ 17 августа 2011

Я нахожу это нелогичным: даже если 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, если только мы не нажмем Nothing, и в этом случае вычисление будет прервано. Это принципиально отличается от вышеописанных случаев, поэтому монадные трансформаторы даже существуют в первую очередь - для их составления наивно не требуется никакого специального механизма, поскольку они работают независимо друг от друга.


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

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

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

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

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

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

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

Да, это похоже на то, чего ожидать, я боюсь.

5 голосов
/ 17 августа 2011

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

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

...