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.
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
.