В чем разница между разными порядками одинаковых монадных трансформаторов? - PullRequest
25 голосов
/ 22 февраля 2011

Я пытаюсь определить API для выражения процедуры определенного типа в моей программе.

newtype Procedure a = { runProcedure :: ? }

Существует состояние, состоящее из сопоставления идентификаторов с записями:

type ID = Int
data Record = { ... }
type ProcedureState = Map ID Record

Существует три основных операции:

-- Declare the current procedure invalid and bail (similar to some definitions of fail for class Monad)
abort :: Procedure ()
-- Get a record from the shared state; abort if the record does not exist.
retrieve :: ID -> Procedure Record
-- Store (or overwrite) a record in the shared state.
store :: ID -> Record -> Procedure ()

У меня есть несколько целей с этими операциями:

  • Процедуры могут делать предположения (в отличие от необработанного вызова Map.lookup), относительно которыхзаписи доступны, и если какое-либо из их предположений неверно, процедура в целом возвращает ошибку.
  • Ряд процедур можно объединить в цепочку, используя <|> (из класса Alternative), чтобы вернуться кПроцедуры, которые делают разные предположения.(По аналогии с orElse STM)

Учитывая эти цели, я считаю, что мне нужна некоторая комбинация State и Maybe монад.

-- Which to choose?
type Procedure a = StateT ProcedureState Maybe a
type Procedure a = MaybeT (State ProcedureState) a

Я могу 'Выяснить, как два порядка Maybe и State будут вести себя по-разному. Может кто-нибудь объяснить разницу в поведении между этими двумя порядками?

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

Заключение: Все три ответа были полезны, но была одна общая идея, которая помогла мне решить, какой порядок мне нужен.Посмотрев тип возврата runMaybeT / runStateT, было легко увидеть, какая комбинация имела то поведение, которое я искал.(В моем случае мне нужен тип возврата Maybe (ProcedureState, a)).

Ответы [ 5 ]

23 голосов
/ 22 февраля 2011

Редактировать: я изначально получил дела задом наперед. Исправлено сейчас.

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

type Procedure a = MaybeT (State ProcedureState) a

В этом случае вы сначала запускаете MaybeT, что приводит к вычислению с состоянием, которое возвращает Maybe a.

type Procedure a = StateT ProcedureState Maybe a

Здесь StateT - это внешняя монада, что означает, что после запуска StateT с начальным состоянием, вы получите Maybe (a, ProcedureState). То есть вычисление может быть успешным или не иметь успеха.

То, что вы выберете, зависит от того, как вы хотите обрабатывать частичные вычисления. С MaybeT снаружи вы всегда получите какое-то возвращаемое состояние независимо от успеха вычислений, что может быть или не быть полезным. С StateT снаружи вы гарантируете, что все транзакции с состоянием действительны. Из того, что вы описываете, я, вероятно, сам использовал бы вариант StateT, но я ожидаю, что любой из них может сработать.

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

13 голосов
/ 05 декабря 2012

Чтобы дополнить другие ответы, я хотел бы описать, как это выяснить в общем случае.То есть, учитывая два преобразователя, какова семантика их двух комбинаций?

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

Шаг 1 : создать таблицу основных типов монад и соответствующих им типов трансформаторов:

transformer           type                  base type (+ parameter order)

---------------------------------------------------------------

MaybeT   m a        m (Maybe a)            b.    Maybe b

StateT s m a        s -> m (a, s)          t b.  t -> (b, t)

ListT    m a        m [a]                  b.    [] b

ErrorT e m a        m (Either e a)         f b.  Either f b

... etc. ...

Шаг 2: примените каждый монадный преобразователь к каждой из базовых монад, подставив вместо параметра типа m:

inner         outer         combined type

Maybe         MaybeT        Maybe (Maybe a)
Maybe         StateT        s -> Maybe (a, s)      --  <==  this !!
... etc. ...

State         MaybeT        t -> (Maybe a, t)      --  <== and this !!
State         StateT        s -> t -> ((a, s), t)
... etc. ...

(Этот шаг немного болезненный, поскольку существует квадратичное числокомбинации ... но это было хорошее упражнение для меня, и мне нужно было сделать это только один раз.) Ключ для меня в том, что я написал комбинированные типы развернутый - без всех этихраздражающие обертки MaybeT, StateT и т. д.Мне намного проще смотреть и думать о типах без шаблона.

Чтобы ответить на ваш оригинальный вопрос, эта диаграмма показывает, что:

  • MaybeT + State :: t -> (Maybe a, t)вычисление с состоянием, в котором не может быть значения, но всегда будет (возможно измененный) вывод состояния

  • StateT + Maybe :: s -> Maybe (a, s) вычисление, при котором и состояние, и значение могут отсутствовать

7 голосов
/ 22 февраля 2011

Давайте представим, что вместо того, чтобы использовать State / StateT для хранения состояния ваших процедур, вы использовали IORef в монаде IO.

Априори есть два способаможет хотеть, чтобы mzero (или fail) вел себя в комбинации монад * IO и Maybe:

  • либо mzero уничтожает все вычисления, так что mzero <|> x = x;или
  • mzero заставляет текущее вычисление не возвращать значение, но эффекты типа IO сохраняются.

Звучит так, как будто вы хотите первое, так чтосостояние, установленное одной процедурой, «разворачивается» для следующей процедуры в цепочке <|> s.

Конечно, эту семантику невозможно реализовать.Мы не знаем, будет ли вычисление вызывать mzero до тех пор, пока мы его не запустим, но это может иметь произвольные IO эффекты, такие как launchTheMissiles, которые мы не можем откатить.

Теперь давайтепопробуйте собрать два разных стека монадных трансформаторов из Maybe и IO:

  • IOT Maybe - упс, этого не существует!
  • MaybeT IO

Существующее (MaybeT IO) дает возможное поведение mzero, а несуществующее IOT Maybe соответствует другому поведению.

К счастью, вы используетеState ProcedureState, чьи эффекты можно откатить, вместо IO;стек монадных трансформаторов, который вам нужен, это StateT ProcedureState Maybe один.

4 голосов
/ 22 февраля 2011

Вы сможете ответить на вопрос самостоятельно, если попытаетесь написать «запускающие» функции для обеих версий - у меня не установлены MTL + -трансформаторы, поэтому я не могу сделать это сам.Один вернет (возможно, состояние) другой Возможно (состояние) .

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

1 голос
/ 18 июня 2018

Сводка: разные заказы на стопку дают различную бизнес-логику

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

При демонстрации влияния заказов люди обычно используют самые простые преобразователи, такие как ReaderT, WriterT, StateT, MaybeT, ExceptT. Разные их порядки не дают принципиально разной бизнес-логики, поэтому трудно ясно понять влияние. Кроме того, некоторые их подмножества являются коммутативными, т. Е. Функциональных различий нет.

В демонстрационных целях я предлагаю использовать StateT и ListT, которые показывают резкое различие между порядками трансформаторов в стеках монад.

Фон: StateT и ListT

  • StateT: State Монада хорошо объяснена в Для нескольких монад Подробнее . StateT просто дает вам немного больше энергии - используя монадические операции его базового m. Достаточно, если вы знаете evalStateT, put, get и modify, что объясняется во многих State руководствах по монадам.
  • ListT: List, a.k.a, [], является монадой (объяснено в Горсть монад ). ListT m a (в пакете list-t) дает вам что-то похожее на [a] плюс все монадические операции базовой монады m. Сложная часть - выполнение ListT (что-то сравнимое с evalStateT): существует множество способов выполнения. Подумайте о различных результатах, которые вам небезразличны при использовании evalStateT, runStateT и execState, в контексте монады List много потенциальных потребителей, таких как , просто выберите их , то есть traverse_, сложите их , т. Е. fold и более.

Эксперимент: понять влияние порядка монадического трансформатора

Мы создадим простой двухслойный стек монадных трансформеров, используя StateT и ListT поверх IO, чтобы выполнить некоторые функции для демонстрации.

Описание задачи

Суммирование чисел в потоке

Поток будет абстрагирован как список Integer с, поэтому приходит наш ListT. Чтобы суммировать их, нам нужно сохранять состояние суммы при обработке каждого элемента в потоке, где наш StateT приходит.

Два стека

У нас есть простое состояние: Int, чтобы сохранить сумму

  • ListT (StateT Int IO) a
  • StateT Int (ListT IO) a

Полная программа

#!/usr/bin/env stack
-- stack script --resolver lts-11.14 --package list-t --package transformers

import ListT (ListT, traverse_, fromFoldable)
import Control.Monad.Trans.Class (lift)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.State (StateT, evalStateT, get, modify)

main :: IO()
main =  putStrLn "#### Task: summing up numbers in a stream"
     >> putStrLn "####       stateful (StateT) stream (ListT) processing"
     >> putStrLn "#### StateT at the base: expected result"
     >> ltst
     >> putStrLn "#### ListT at the base: broken states"
     >> stlt



-- (ListT (StateT IO)) stack
ltst :: IO ()
ltst = evalStateT (traverse_ (\_ -> return ()) ltstOps) 10

ltstOps :: ListT (StateT Int IO) ()
ltstOps = genLTST >>= processLTST >>= printLTST

genLTST :: ListT (StateT Int IO) Int
genLTST = fromFoldable [6,7,8]

processLTST :: Int -> ListT (StateT Int IO) Int
processLTST x = do
    liftIO $ putStrLn "process iteration LTST"
    lift $ modify (+x)
    lift get

printLTST :: Int -> ListT (StateT Int IO) ()
printLTST = liftIO . print



-- (StateT (ListT IO)) stack
stlt :: IO ()
stlt = traverse_ (\_ -> return ())
     $ evalStateT (genSTLT >>= processSTLT >>= printSTLT) 10

genSTLT :: StateT Int (ListT IO) Int
genSTLT = lift $ fromFoldable [6,7,8]

processSTLT :: Int -> StateT Int (ListT IO) Int
processSTLT x = do
    liftIO $ putStrLn "process iteration STLT"
    modify (+x)
    get

printSTLT :: Int -> StateT Int (ListT IO) ()
printSTLT = liftIO . print

Результаты и объяснение

$ ./order.hs   
#### Task: summing up numbers in a stream
####       stateful (StateT) stream (ListT) processing
#### StateT at the base: expected result
process iteration LTST
16
process iteration LTST
23
process iteration LTST
31
#### ListT at the base: broken states
process iteration STLT
16
process iteration STLT
17
process iteration STLT
18

Первый стек ListT (StateT Int IO) a дает правильный результат, поскольку StateT вычисляется после ListT. При оценке StateT исполняющая система уже оценила все операции ListT - заполняя стек потоком [6,7,8], проходя через них traverse_. Слово оцененный здесь означает, что эффекты ListT исчезли, а ListT теперь прозрачен для StateT.

Второй стек StateT Int (ListT IO) a не имеет правильного результата, поскольку StateT слишком короток. На каждой итерации ListT оценки, a.k.a., traverse_, состояние создается, оценивается и исчезает. StateT в этой структуре стека не достигает своей цели сохранения состояний между операциями элемента списка / потока.

...