Search code examples
haskellrecursioniostatemonads

Maintaining state during interactive command line prompt


I want to write a toy program that has an interactive prompt and that can save and display all previous inputs. This is my first attempt, but does not compile (using 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 ()

Note: line 9 is putStr prompt The same error is given for lines 10, 12, 22, 27.

I have since thought of doing the recursion purely inside parse_input, in which case I don't seem to need the state monad. But I am still curious why I get the compilation error. Any help is appreciated, I am very new to Haskell.


Solution

  • You seem to be mixing values of type State s a with values of type IO a. In your main action, you call parse_input in a context expecting IO. In parse_input, you call putStr and so on in a context expecting State. That's not going to work!

    The usual way to do this kind of thing is to switch from State to StateT, and import Control.Monad.IO.Class. Now, you can use

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

    to "lower" your loop to IO, and

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

    to "lift" the IO actions to StateT within the loop. Now (untested code ahead):

    -- 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 ()
    

    As Daniel Wagner points out, this will not preserve the state from one main run to the next. If that's your intention, you can write

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

    If you like, you can import Control.Monad and shorten this to

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

    Final note: if you want to capture the final state of your loop, use runStateT instead of evalStateT.