Стек, который вы написали, немного странный, потому что он параметризован m
слева, но специализирован для IO
справа, поэтому давайте посмотрим на полностью m
-параметризованный вариант:
newtype Stack s m a = Stack { runStack :: ReaderT s (StateT s m) a }
Теперь runStack
вот только имя поля, поэтому мы можем отбросить его и написать эквивалентное newtype
определение:
newtype Stack s m a = Stack (ReaderT s (StateT s m) a)
У нас также есть следующие определения новых типов библиотеки, пропускающие имена полей. Я также использовал свежие переменные, поэтому мы не делаем глупостей, таких как путаница двух a
в разных областях при расширении:
newtype ReaderT r1 m1 a1 = ReaderT (r1 -> m1 a1)
newtype StateT s2 m2 a2 = StateT (s2 -> m2 (a2, s2))
Конечно, если нас интересует только тип с точностью до изоморфизма, то обертки нового типа не имеют значения, поэтому просто переписайте их как псевдонимы типов:
type Stack s m a = ReaderT s (StateT s m) a
type ReaderT r1 m1 a1 = r1 -> m1 a1
type StateT s2 m2 a2 = s2 -> m2 (a2, s2)
Теперь легко расширить тип Stack
:
Stack s m a
= ReaderT s (StateT s m) a
-- expand ReaderT with r1=s, m1=StateT s m, a1=a
= s -> (StateT s m) a
= s -> StateT s m a
-- expand StateT with s2=s m2=m a2=a
= s -> (s -> m (a, s))
= s -> s -> m (a, s)
Как заметил @duplode, здесь нет лишних a
.
Интуитивно понятно, что Stack
читает из s
(первый аргумент), принимает начальное состояние типа s
(второй аргумент) и возвращает монадическое действие в m
(например, IO
), которое может вернуть значение типа a
и обновленное состояние типа s
.