Стек преобразователя Haskell Monad и подписи типов

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

Стек объединяет несколько преобразователей StateT, поскольку у меня есть несколько состояний, которые мне нужно отслеживать (два из которых можно объединить в кортежи, но я вернусь к этому через секунду), и WriterT для ведения журнала.

Вот что у меня есть на данный момент:

module Pass1 where
import Control.Monad.Identity
import Control.Monad.State
import Control.Monad.Writer
import Data.Maybe
import qualified Data.Map as Map
import Types

data Msg = Error String
         | Warning String

type Pass1 a = WriterT [Msg] (StateT Int (StateT [Line] (StateT [Address] Identity))) a


runPass1 addrs instrs msgs = runIdentity (runStateT (runStateT (runStateT (runWriterT msgs) 1) instrs) addrs)


--popLine :: (MonadState s m) => m (Maybe s)
--popLine :: (Monad m) => StateT [Line] m (Maybe Line)
popLine :: (MonadState s m) => m (Maybe Line)
popLine = do
        ls <- get
        case ls of
          x:xs -> do
                    put xs
                    return $ Just x
          []   -> return Nothing


incLineNum :: (Num s, MonadState s m) => m ()
incLineNum = do
               ln <- get
               put $ ln + 1

curLineNum :: (MonadState s m) => m s
curLineNum = do
               ln <- get
               return ln

evalr = do l <- popLine
           --incLineNum
           return l

Я бы хотел, чтобы popLine возился с состоянием [Line], а функции xLineNum повлияли на состояние Int. evalr - это вычисление, которое будет передано в runPass1.

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

Pass1.hs:23:14:
    No instance for (MonadState [t] m)
      arising from a use of `get' at Pass1.hs:23:14-16
    Possible fix: add an instance declaration for (MonadState [t] m)
    In a stmt of a 'do' expression: ls <- get
    In the expression:
        do ls <- get
           case ls of {
             x : xs -> do ...
             [] -> return Nothing }
    In the definition of `popLine':
        popLine = do ls <- get
                     case ls of {
                       x : xs -> ...
                       [] -> return Nothing }


Pass1.hs:22:0:
    Couldn't match expected type `s' against inferred type `[Line]'
      `s' is a rigid type variable bound by                        
          the type signature for `popLine' at Pass1.hs:21:23        
    When using functional dependencies to combine                  
      MonadState [Line] m,                                         
        arising from a use of `get' at Pass1.hs:23:14-16            
      MonadState s m,                                              
        arising from the type signature for `popLine'              
                     at Pass1.hs:(22,0)-(28,31)                     
    When generalising the type(s) for `popLine'         




Pass1.hs:23:14:
    Could not deduce (MonadState [Line] m)
      from the context (MonadState s m)   
      arising from a use of `get' at Pass1.hs:23:14-16
    Possible fix:                                    
      add (MonadState [Line] m) to the context of    
        the type signature for `popLine'             
      or add an instance declaration for (MonadState [Line] m)
    In a stmt of a 'do' expression: ls <- get
    In the expression:
        do ls <- get
           case ls of {
             x : xs -> do ...
             [] -> return Nothing }
    In the definition of `popLine':
        popLine = do ls <- get
                     case ls of {
                       x : xs -> ...
                       [] -> return Nothing }

Кажется, что ни одна из сигнатур не верна, но popLine - первая функция, поэтому она единственная, которая сразу вызывает ошибку.

Я пытаюсь добавить то, что он предлагает, в сигнатуре типа (например: popLine :: (MonadState [Line] m) => ..., но затем возникает такая ошибка:

Pass1.hs:21:0:
    Non type-variable argument in the constraint: MonadState [Line] m
    (Use -XFlexibleContexts to permit this)                          
    In the type signature for `popLine':                             
      popLine :: (MonadState [Line] m) => m (Maybe Line)

Мне всегда кажется, что это сообщение появляется всякий раз, когда я пытаюсь сделать что-то, кроме переменной типа. Кажется, нравится (MonadState s m) нормально и ошибка в другом, но когда я пробую это с [a] вместо s, появляются ошибки, похожие на указанные выше. (Первоначально [Line] и Int были объединены в кортеж в одном состоянии, но я получал эту ошибку, поэтому решил попробовать поместить их в отдельные состояния).

GHC 6.10.4, Кубунту

Итак, может ли кто-нибудь рассказать мне, что происходит, и дать объяснение / показать мне правильные типовые подписи, или кто-нибудь знает хорошую ссылку на этот материал (единственное, что помогло до сих пор, это «Монадные трансформеры, шаг за шагом» , но при этом используется только одна функция состояния aux и одно StateT)?

Спасибо заранее.

Изменить
Вот код компиляции, включающий предложения JFT и Эдварда:

{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- needed for: deriving (Functor,Monad)
{-# LANGUAGE MultiParamTypeClasses #-}      -- needed for: MonadState instance
{-# LANGUAGE FlexibleContexts #-}           -- needed for: (MonadState PassState m) => ...

module Pass1 where
import Control.Monad.State
import Control.Monad.Writer
import Data.Maybe
import Types

type Lines     = [Line]
type Addresses = [Address]
type LineNum   = Int
type Messages  = [Msg]
data Msg = Error String
         | Warning String

data PassState = PassState { passLineNum :: LineNum
                           , passLines :: Lines
                           , passAddresses :: Addresses
                           }

newtype Pass1 a = Pass1 { unPass1 :: WriterT Messages (State PassState) a
                        }
                        deriving (Functor,Monad)

instance MonadState PassState Pass1 where
        get   = Pass1 . lift $ get
        put s = Pass1 . lift $ put s



runPass1 :: PassState -> Pass1 a -> ((a, Messages), PassState)
runPass1 state = flip runState state .
                 runWriterT          .
                 unPass1


curLineNum :: (MonadState PassState m) => m LineNum
curLineNum = do
               state <- get
               return $ passLineNum state


nextLine :: (MonadState PassState m) => m (Maybe Line)
nextLine = do
             state <- get
             let c = passLineNum state
             let l = passLines state
             case l of
               x:xs -> do
                         put state { passLines = xs, passLineNum = (c+1) }
                         return $ Just x
               _ -> return Nothing



evalr :: Pass1 (Maybe Line,LineNum)
evalr = do
          l <- nextLine
          c <- curLineNum
          --tell $ Warning "hello"
          return (l,c)

Я объединил incLineNum и popLine в nextLine. Мне все еще нужно, чтобы монада Writer работала, но думаю, я знаю, что делать дальше. Спасибо, парни.


person paul    schedule 17.01.2010    source источник


Ответы (2)


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

module Pass1_JFT where
import Control.Monad.Identity
import Control.Monad.State
import Control.Monad.Writer
import Data.Maybe
import qualified Data.Map as Map

{- замена ваших типов импорта простыми определениями -}

--import Types
type Line       = String
type Address    = String
type LineNumber = Int

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

type Lines     = [Line]
type Addresses = [Address]
type Messages  = [Msg]


data Msg = Error String
         | Warning String

{- Что это за Int в StateT Int? Назовите его легче читать, рассуждать и изменять. Декларативная FTW, давайте вместо этого будем использовать LineNumber -}

--type Pass1 a = WriterT [Msg] (StateT Int (StateT [Line] (StateT [Address] Identity))) a

{- Давайте использовать "настоящий" тип, чтобы можно было получить экземпляры. Поскольку Pass1 не является передачей монады, т.е. не определен как Pass1 m a, нет смысла использовать StateT для самого глубокого StateT, то есть StateT [Address] Identity, поэтому давайте просто используем State [Address] -}

newtype Pass1 a = Pass1 {
    unPass1 :: WriterT Messages (StateT LineNumber (StateT Lines (State Addresses))) a
                        }
                        deriving (Functor,Monad)

--runIdentity (runStateT (runStateT (runStateT (runWriterT msgs) 1) instrs) addrs)

{- Давайте очистим этот стек от самого внешнего (самого левого в объявлении) до самого внутреннего идентификатора в вашем исходном объявлении. Обратите внимание, что runWriterT НЕ принимает начальное состояние ... Первый параметр для runStateT (и runState) - это не начальное состояние, а монада ... так что давайте перевернем! -}

runPass1' :: Addresses -> Lines -> Messages -> Pass1 a ->  ((((a, Messages), LineNumber), Lines), Addresses)
runPass1' addrs instrs msgs = flip runState addrs   .
                              flip runStateT instrs .
                              flip runStateT 1      .
                              runWriterT            . -- then get process the WriterT (the second outermost)
                              unPass1                 -- let's peel the outside Pass1

{- теперь последняя функция НЕ делает то, что вы хотите, поскольку вы хотите предоставить начальный журнал для добавления к WriterT. Поскольку это преобразователь монад, мы сделаем здесь кое-что -}

-- I keep the runStateT convention for the order of the arguments: Monad then state
runWriterT' :: (Monad m,Monoid w) => WriterT w m a -> w -> m (a,w)
runWriterT' writer log = do
    (result,log') <- runWriterT writer
    -- let's use the monoid generic append in case you change container...
    return (result,log `mappend` log')

runPass1 :: Addresses -> Lines -> Messages -> Pass1 a ->  ((((a, Messages), LineNumber), Lines), Addresses)
runPass1 addrs instrs msgs = flip runState addrs   .
                             flip runStateT instrs .
                             flip runStateT 1      .
                             flip runWriterT' msgs . -- then get process the WriterT (the second outermost)
                             unPass1                 -- let's peel the outside Pass1

{- Вы собираетесь вызывать popLine прямо из стека Pass1? Если это так, вам нужно «научить» Pass1 быть «MonadState Lines». Для этого давайте выведем Pass1 (поэтому мы объявили его с помощью newtype!) -}

instance MonadState Lines Pass1 where
    -- we need to dig inside the stack and "lift" the proper get
    get   = Pass1 . lift . lift $ get
    put s = Pass1 . lift . lift $ put s

{- Лучше сохранить общий вид, но теперь мы могли бы написать: popLine :: Pass1 (Maybe Line) -}

popLine :: (MonadState Lines m) => m (Maybe Line)
popLine = do
        ls <- get
        case ls of
          x:xs -> do
                    put xs
                    return $ Just x
          []   -> return Nothing

{- Хорошо, теперь я получаю Int => LineNumber .... мы могли бы сделать Pass1 и экземпляр MonadState LineNumber, но LineNumber не следует путать, поэтому вместо этого я бы напрямую закодировал incLine и при необходимости предоставил бы экземпляр MonadReader для консультации

check ":t incLineNum and :t curLineNum"

-}

incLineNum = Pass1 . lift $ modify (+1)

curLineNum = Pass1 $ lift get

evalr = do l <- popLine
           incLineNum
           return l

Это длинный ответ, но монада и стек монад, как вы видите, поначалу являются сложными. Я исправил код, но я рекомендую вам поиграть и проверить типы различных функций, чтобы понять, что происходит, и сравнить с вашим оригиналом. Вывод типа в Haskell означает, что обычно аннотации типов излишни (если только не устранить двусмысленность). В общем, тип, который мы даем функции, менее общий, чем предполагаемый, поэтому лучше не вводить аннотацию. Однако аннотация типов - определенно хороший метод отладки;)

Ваше здоровье

P.S. Глава Real World Haskell о преобразователе монад превосходна: http://book.realworldhaskell.org/read/monad-transformers.html

person JFT    schedule 18.01.2010
comment
Героизм надо вознаграждать. +1 - person Norman Ramsey; 18.01.2010
comment
Потрясающе, спасибо вам ОЧЕНЬ! Пошаговое объяснение - вот что мне нужно, спасибо, что нашли время сделать это! Ваши предложения по стилю также были высоко оценены. Да, RWH - отличная книга, прямо сейчас передо мной лежит мой экземпляр. Я думаю, что моя проблема в том, чтобы читать его слишком быстро - это такой красивый язык, мне не терпится выучить его! (кстати, для тех, кто пытается запустить код JFT, эти расширения ghc должны быть включены :) {- # LANGUAGE GeneralizedNewtypeDeriving # -} {- # LANGUAGE TypeSynonymInstances # -} {- # LANGUAGE FlexibleContexts # -} {- # LANGUAGE MultiParamTypeClasses # -} - person paul; 19.01.2010
comment
Кроме того, изящный трюк, помещающий ваши комментарии в {- и -}, я должен помнить об этом при публикации сообщений на досках сообщений. Делает копирование кода для запуска очень простым. - person paul; 19.01.2010
comment
Рад быть полезным :) Извините за директиву LANGUAGE, я сохранил их в своем .ghci и забыл о них. Я обычно использовал glasgow-exts, Arrows и BangPattern. - person JFT; 19.01.2010

В общем, вы обнаружите, что код становится намного яснее, используя одно StateT с более крупной составной структурой для всех битов состояния, которые вам нужны. Одна из веских причин заключается в том, что когда вы придумываете часть состояния, о которой вы забыли, вы всегда можете увеличить структуру на одно поле, и вы можете использовать сахар записи для записи обновлений одного поля или обратиться к чему-то вроде fclabels или data-accessor пакеты для управления состоянием.

data PassState = PassState { passLine :: Int, passLines :: [Line] }

popLine :: MonadState PassState m => m (Maybe Line).   
popLine = do
   state <- get
   case passLines state of
      x:xs -> do 
         put state { passLines = xs }
         return (Just x)
      _ -> return Nothing
person Edward KMETT    schedule 18.01.2010
comment
Хороший вызов, помещающий все переменные состояния в один тип данных, мне не приходило в голову. (Я все еще привыкаю к ​​Haskell; по какой-то причине у меня возникает туннельное зрение, когда я думаю об этом.) Я думал поместить их в кортеж, но хотел избежать беспорядочного кода, который мог бы вызвать. Таким образом, сложенные файлы StateT. Но ваше предложение проясняет это! Хорошее доказательство будущего, как вы и сказали. Спасибо! - person paul; 19.01.2010
comment
Когда биты информации, которую вы отслеживаете, относятся к одной и той же концепции, это действительно очень хорошая идея (например, состояние синтаксического анализа). С другой стороны, есть случаи, когда вы предпочитаете, чтобы отслеживание было ортогональным, а значит, более удобным для повторного использования. Мой DSL на работе отслеживает символы, присвоение типов, среду, журнал и многое другое. Все они ортогональны, поэтому у меня есть стек монад для разделения этих проблем, и я фактически повторно использовал часть стека в различных областях системы, которые не нуждаются в полном стеке. - person JFT; 19.01.2010
comment
JFT: конечно, проблема в том, что вы попадаете в среду, в которой вам нужно знать магическое количество «подъемов», чтобы добраться до вашего MonadState и делать правильные вещи, или вы тратите все свое время на возню с шумом newtype. Подход составного состояния может быть расширен путем создания класса типов полей, которые вы хотите иметь в нескольких формах состояния, или путем применения двойного подхода `` типы данных по выбору '' и создания чего-то вроде (StateT ( Lines: *: LineCount)) m, но это становится слишком похоже на OOHaskell, чтобы я был полностью им доволен. - person Edward KMETT; 19.01.2010