在交互式命令行提示期间保持状态

Maintaining state during interactive command line prompt

我想写一个玩具程序,它有一个交互式提示,可以保存和显示所有以前的输入。这是我的第一次尝试,但没有编译(使用 ghc):

import System.IO
import Control.Monad.State

data ProgramState = ProgramState
    { events :: [Int] }     -- Placeholder for now

parse_input :: String -> State ProgramState Bool
parse_input prompt = do
    putStr prompt
    hFlush stdout
    current_state <- get
    str <- getLine
    case str of
        "c" -> do 
            put (current_state { events = [1,2,3] } )   -- this should become actual appending
            return True
        "l" -> return True    
        "q" -> return False  
        "quit" -> return False
        "h" -> return True
        _ -> do 
            putStrLn "Invalid input."
            parse_input prompt

main :: IO ()
main = do
    should_continue <- parse_input "Enter your command."
    if should_continue then main else return ()
main.hs:9:5: error:                                                                                                                                                                                                                                                            
    • Couldn't match type ‘IO’                                                                                                                                                                                                                                                 
                     with ‘StateT ProgramState Data.Functor.Identity.Identity’                                                                                                                                                                                                 
      Expected type: StateT                                                                                                                                                                                                                                                    
                       ProgramState Data.Functor.Identity.Identity ()                                                                                                                                                                                                          
      Actual type: IO ()

注意:第 9 行是 putStr prompt 第 10、12、22、27 行给出了相同的错误。

我已经考虑纯粹在 parse_input 内部进行递归,在这种情况下我似乎不需要状态 monad。但是我仍然很好奇为什么会出现编译错误。感谢任何帮助,我是 Haskell.

的新手

您似乎将 State s a 类型的值与 IO a 类型的值混合在一起。在您的 main 操作中,您在期望 IO 的上下文中调用 parse_input。在 parse_input 中,您在需要 State 的上下文中调用 putStr 等等。那是行不通的!

通常做这种事情的方法是从State切换到StateT,然后导入Control.Monad.IO.Class。现在,您可以使用

evalStateT :: StateT s m a -> s -> m a

将循环“降低”到 IO,并且

-- liftIO :: IO a -> StateT s IO a
liftIO :: MonadIO m => IO a -> m a

将循环中的 IO 动作“提升”到 StateT。现在(前面未经测试的代码):

-- Needed for flexible use of
-- the MonadState class.
{-# LANGUAGE FlexibleContexts #-}

import System.IO
-- You almost always want the "strict"
-- version of `StateT`; the lazy one is weird.
import Control.Monad.State.Strict
import Control.Monad.IO.Class

data ProgramState = ProgramState
    { events :: [Int] }     -- Placeholder for now

-- Renaming your function to follow convention.
parseInput
  :: (MonadState ProgramState m, MonadIO m)
  => String -> m Bool
parseInput prompt = do
    str <- liftIO $ do
      putStr prompt
      hFlush stdout
      getLine
    current_state <- get
    case str of
        "c" -> do 
            put (current_state { events = [1,2,3] } )   -- this should become actual appending
            return True
        "l" -> return True    
        "q" -> return False  
        "quit" -> return False
        "h" -> return True
        _ -> do 
            liftIO $ putStrLn "Invalid input."
            parseInput prompt

main :: IO ()
main = do
    -- You need to supply the initial state; I've just guessed here.
    should_continue <- evalStateT (parseInput "Enter your command.") (ProgramState [])
    if should_continue then main else return ()

正如 Daniel Wagner 指出的那样,这不会保留一个 main 运行 下一个的状态。如果那是你的意图,你可以写

main :: IO ()
main = evalStateT loop (ProgramState [])
  where
    loop = do
      should_continue <- parseInput "Enter your command."
      if should_continue then loop else return ()

如果您愿意,可以导入 Control.Monad 并将其缩短为

main :: IO ()
main = evalStateT loop (ProgramState [])
  where
    loop = do
      should_continue <- parseInput "Enter your command."
      when should_continue loop

最后说明:如果您想捕获循环的最终状态,请使用 runStateT 而不是 evalStateT