Есть ли существенная разница между StateT над Reader и ReaderT над State?

Когда я разрабатываю свою модель программирования, у меня всегда возникает дилемма, какой подход лучше:

type MyMonad1 = StateT MyState (Reader Env)
type MyMonad2 = ReaderT Env (State MyState)

Каковы преимущества и компромиссы между использованием одной монады над другой? Имеет ли это вообще значение? Как насчет производительности?


person radrow    schedule 02.06.2019    source источник
comment
Я не эксперт в Monad Transformers, но, насколько я могу судить, здесь это не имеет большого значения. По модулю новых типов MyMonad1 a совпадает с MyState -> Env -> (a, MyState), а MyMonad2 a — с Env -> MyState -> (a, MyState), поэтому единственная разница — это порядок аргументов.   -  person Robin Zigmond    schedule 02.06.2019
comment
Это вопрос конкретно о ReaderT и StateT или вообще о том, как выбрать порядок трансформаторов в стеке?   -  person Daniel Wagner    schedule 02.06.2019
comment
Конкретно об этих двух   -  person radrow    schedule 02.06.2019


Ответы (1)


В общем случае разные порядки преобразователей монад приведут к разному поведению, но, как было указано в комментариях, для двух порядков «состояние» и «читатель» мы имеем следующие изоморфизмы вплоть до новых типов:

StateT MyState (Reader Env) a  ~  MyState -> Env -> (a, MyState)
ReaderT Env (State MyState) a  ~  Env -> MyState -> (a, MyState)

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

Что касается производительности, трудно сказать наверняка, не сравнив фактический код. Однако в качестве одной точки данных, если вы рассматриваете следующее монадическое действие:

foo :: StateT Double (Reader Int) Int
foo = do
  n <- ask
  modify (* fromIntegral n)
  gets floor

затем при компиляции с GHC 8.6.4 с использованием -O2 новые типы, очевидно, оптимизируются, и это генерирует точно то же ядро, если вы измените подпись на:

foo :: ReaderT Int (State Double) Int

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

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

Без веской причины я предпочитаю № 2, в основном потому, что Env -> MyState -> (a, MyState) выглядит для меня более естественным.

person K. A. Buhr    schedule 03.06.2019
comment
Небольшое замечание: просто иметь два изоморфных типа недостаточно, когда мы говорим об удалении новых типов; также необходимо проверить, что связанные экземпляры соблюдают изоморфизм. (Например, рассмотрим All и гипотетический Xor, которые разворачиваются в Bool, но чьи экземпляры Monoid не соблюдают ни изоморфизм id, ни not.) Однако в данном случае это очень незначительная придирка, поскольку экземпляры действительно соответствуют соответственно. - person Daniel Wagner; 04.06.2019