Подтвердить что ты не робот

Как создать монодичный стек?

Как вы разрабатываете и создаете свои монадические стеки? Впервые мне нужно построить монодичный стек (используя трансформаторы), чтобы решить проблему реального мира, но я не совсем уверен, в каком порядке складывать трансформаторы. Как вы уже знаете, до тех пор, пока вычисление имеет добрый * -> *, в принципе все может играть роль внутренней монады в трансформаторе, таким образом, пара вопросов:

  • Если какой-то конкретный трансформатор находится в верхней части стека (например, ReaderT? WriterT?)
  • Что должно водить дизайн? Интуиция? Типы? (например, сформируйте стек в соответствии с вашими требованиями API)
  • Является ли каждый стек изоморфным друг другу (в определенной степени), или это вероятно, что, если я создам свой стек неправильно, я могу закончить до невозможности использовать определенные базовые монады или иметь большой раздутый беспорядок lift . lift . liftIO [...]? Мое чувство кишки предполагает, что если трансформаторы получат некоторые экземпляры (например, MonadReader, MonadIO и т.д., Как и большинство трансформаторов в mtl do), не имеет значения, в каком порядке я помещал трансформаторы.

Мне интересно услышать от опытных Haskellers о лучших практиках или эмпирических правилах.

forever $ print "Thanks!"

а.

4b9b3361

Ответ 1

Требуется опыт. Следует помнить, что трансформатор монады ничего не знает о монаде, которую он трансформирует, поэтому внешняя "привязана" к внутреннему поведению. Так

StateT s (ListT m) a

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

Контраст с ListT (StateT s m) a, который в основном является состоянием, т.е. будет только одно состояние для всего вычисления (по модулю m), и вычисление будет действовать "однопоточно" в состоянии, потому что то, что State означает. Недетерминизм будет выше этого - так что ветки смогут наблюдать изменения состояния предыдущих неудавшихся ветвей. (В этой конкретной комбинации это действительно странно, и я никогда не нуждался в ней).

Вот диаграмма Дан Пипони, которая дает некоторую полезную интуицию:

monad doodles

Я также считаю полезным перейти к типу реализации, чтобы дать мне понять, что это за расчет. 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 находится внутри, если вычисление завершается неудачно, вы не получаете состояние, поэтому вам нужно прервать любые изменения состояния, которые произошли при неудачном вычислении. Какой из них вы хотите, зависит от того, что вы пытаетесь сделать - первое, однако, соответствует интуиции императивных программистов. (Не то, что обязательно нужно что-то делать)

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

Ответ 2

Это довольно широкий вопрос. Я просто дам вам некоторые основные идеи для работы.

Прежде всего, я предлагаю сохранить полиморфную базовую монаду, где это возможно. Это позволит вам повторно использовать код как в чистых, так и в IO-настройках. Это также сделает ваш код более сложным. Использование различных классов, таких как MonadIO, также может помочь сохранить ваш код более полиморфным, что обычно хорошо.

Важно отметить, что порядок ваших монадных трансформаторов фактически контролирует их семантику. Мой любимый пример сочетает что-то вроде ListT ¹ с EitherT для обработки ошибок. Если у вас есть ListT снаружи, все вычисления могут завершиться с ошибкой. Если у вас есть EitherT снаружи, каждая ветка может выйти из строя отдельно. Таким образом, вы можете фактически контролировать, как ошибки взаимодействуют с недетерминированностью, просто изменяя порядок ваших трансформаторов!

Если используемые вами монадные трансформаторы не зависят от порядка - например. это не имеет большого значения для объединения ReaderT и WriterT, я считаю, - тогда просто играйте на ухо и идите с тем, что кажется лучшим для вашего приложения. Это тот выбор, который станет легче с опытом.

¹: ListT from Control.Monad.Trans имеет некоторые проблемы, поэтому предположим, что ListT сделано правильно.