Понимание сигнатур типа Monad Transformer

#haskell #functional-programming

#haskell #функциональное программирование

Вопрос:

В настоящее время я занимаюсь monad transformer и пытаюсь действительно понять сигнатуры типов, и у меня возникает некоторая путаница. Давайте используем следующий стек для обсуждения:

 newtype Stack s m a = Stack { runStack :: ReaderT s (StateT s IO) a }
  

Я пытаюсь проходить слой за слоем и записывать подписи развернутого типа, но застреваю:

 newtype Stack s m a = Stack { 
    runStack :: ReaderT s         (StateT s IO)       a }
--              ReaderT s                  m          a
--                      s ->               m          a
--                      s ->         (StateT s IO)    a
--                            StateT  s     m         a
--                      s ->         (s -> IO (a, s)) a  
  

Это просто не похоже на действительную сигнатуру возвращаемого типа в последней строке, по сути, у нас есть функция, которая принимает s и возвращает функцию, сопоставленную с a ?

Я понимаю, что внутренняя функция в конечном итоге преобразуется в монаду, и именно поэтому это m in ReaderT r m a , но это напрягает мой мозг.

Может ли кто-нибудь предложить какое-либо представление, правильно ли я проанализировал типы, и я просто должен признать, что s -> (s -> IO (a, s)) a это действительно так?

Спасибо

Комментарии:

1. У нас есть newtype StateT s m a = StateT { runStateT :: s -> m (a,s) } . Поскольку это так, (StateT s IO) a все сводится к s -> IO (a,s) , и никаких зависаний нет a .

2. @duplode что насчет того, a что возвращает ReaderT? newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a } . Потому что IO (a,s) есть a в кортеже, который был удален из крайнего правого, как у меня выше?

3. «что насчет того, a что возвращает ReaderT?» — Это a в (StateT s IO) a . Обратите внимание, что ReaderT это возвращает не an a само по себе, а скорее m a .

4. @duplode : потрясающе: теперь это имеет полный смысл, вау, большое спасибо, что прояснили это для меня!!

Ответ №1:

Стек, который вы написали, немного странный, потому что он параметризован 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 s в разных областях при расширении:

 newtype ReaderT r1 m1 a1 = ReaderT (r1 -> m1 a1)
newtype StateT s2 m2 a2 = StateT (s2 -> m2 (a2, s2))
  

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

 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 .

Комментарии:

1. Ваш ответ был очень полезен, я действительно ценю это, спасибо!