Ответ 1
Требуется опыт. Следует помнить, что трансформатор монады ничего не знает о монаде, которую он трансформирует, поэтому внешняя "привязана" к внутреннему поведению. Так
StateT s (ListT m) a
является, прежде всего, недетерминированным вычислением из-за внутренней монады. Тогда, беря недетерминизм как нормальный, вы добавляете состояние - то есть каждая "ветвь" недетерминизма будет иметь свое собственное состояние.
Контраст с ListT (StateT s m) a
, который в основном является состоянием, т.е. будет только одно состояние для всего вычисления (по модулю m
), и вычисление будет действовать "однопоточно" в состоянии, потому что то, что State
означает. Недетерминизм будет выше этого - так что ветки смогут наблюдать изменения состояния предыдущих неудавшихся ветвей. (В этой конкретной комбинации это действительно странно, и я никогда не нуждался в ней).
Вот диаграмма Дан Пипони, которая дает некоторую полезную интуицию:
Я также считаю полезным перейти к типу реализации, чтобы дать мне понять, что это за расчет. ListT
трудно развернуть, но вы можете видеть его как "undefined", а StateT
легко расширяться. Итак, для приведенного выше примера я бы посмотрел
StateT s (ListT m) a =~ s -> ListT m (a,s)
т.е. он принимает входящее состояние и возвращает много исходящих состояний. Это дает вам представление о том, как это будет работать. Аналогичный подход заключается в том, чтобы посмотреть тип функции run
, который вам нужен для вашего стека, - соответствует ли это вашей информации и необходимой информации?
Вот некоторые эмпирические правила. Они не заменяют время, чтобы выяснить, какой из них вам действительно необходим, расширяя и просматривая, но если вы просто ищете "добавление функций" в каком-то императивном смысле, тогда это может быть полезно.
ReaderT
, WriterT
и StateT
являются наиболее распространенными трансформаторами. Во-первых, все они коммутируют друг с другом, поэтому неважно, в какой порядок вы их вставляете (рассмотрите возможность использования RWS
, если вы используете все три). Кроме того, на практике я обычно хочу, чтобы они были снаружи, с более богатыми трансформаторами типа ListT
, LogicT
и ContT
внутри.
ErrorT
и MaybeT
обычно идут снаружи трех выше; посмотрим, как MaybeT
взаимодействует с StateT
:
MaybeT (StateT s m) a =~ StateT s m (Maybe a) =~ s -> m (Maybe a, s)
StateT s (MaybeT m) a =~ s -> MaybeT m (a,s) =~ s -> m (Maybe (a,s))
Когда MaybeT
находится снаружи, изменение состояния наблюдается, даже если вычисление не выполняется. Когда MaybeT
находится внутри, если вычисление завершается неудачно, вы не получаете состояние, поэтому вам нужно прервать любые изменения состояния, которые произошли при неудачном вычислении. Какой из них вы хотите, зависит от того, что вы пытаетесь сделать - первое, однако, соответствует интуиции императивных программистов. (Не то, что обязательно нужно что-то делать)
Надеюсь, это дало вам представление о том, как думать о стеках трансформаторов, поэтому у вас есть больше инструментов для анализа того, как должен выглядеть ваш стек. Если вы идентифицируете проблему как монадическое вычисление, получение права монады является одним из наиболее важных решений, и это не всегда легко. Не торопитесь и изучите возможности.