Niklas Vest
Niklas Vest

Reputation: 902

State with IO in Haskell for CLI application

I am trying to write a simple implementation of the game Hangman in Haskell and one problem I have is keeping the state of the game.

A simplified / abstract version of my problem looks like this:

advanceState :: Char -> [Char] -> [Char]
advanceState = (:)

My question seems simple but I can't figure out the answer: How can I invoke this function after each character supplied via the standard input and how can I terminate the loop once a certain set of characters was read?

Note that I have also tried versions with State [Char] a and StateT [Char] IO a to no avail.

Upvotes: 2

Views: 224

Answers (2)

assembly.jc
assembly.jc

Reputation: 2076

Provided that don't know how do you to keep the state. Here just provide a simple example show how to use StateT with reading characters from standard input.

Firstly, to define the advanceState function as:

advanceState::StateT [Char] IO [Char]
advanceState = StateT readInput where
    readInput cs = do c <- getChar
                      let newState = (c:cs)
                      return (newState, newState)

The function embedded in StateT is readInput, it has type:

[Char]->IO ([Char], [Char])

The first element of pair ([Char], [Char]) is input that what we want, and the second element of that is the "state" will pass to readInput whenever advanceState is called again.

Now, below is an example show how to use advanceState:

getChar10::StateT [Char] IO [Char]
getChar10 = do s <- advanceState
               if (length s) <= 10
               then getChar10
               else do put []
                       return (reverse s)

This example show how to get 10 character from standard input when the user press enter, note that put [] reset the state so that next call of advanceState will not has the previous input.

Finally, we can play around with them:

playState::IO ()
playState = print =<< evalStateT loop [] where
    loop  = do s1 <- getChar10
               s2 <- getChar10
               return (s1 ++ "::" ++ s2)  

The playState function will print first 20 characters (included new line character \n, \r) whatever how many characters user has inputted before press enter. If input not enough 20 characters, playState will request user to input more characters.

Upvotes: 1

Shersh
Shersh

Reputation: 9179

A simple solution to this problem which doesn't involve using monads besides IO is to use inner loop function which keeps its state as an argument. I'm going to provide an artificial example below:

myProgram :: IO [Char]
myProgram = loop []
  where
    loop :: [Char] -> IO [Char]
    loop state = do
        input <- getLine
        case input of
            "A" -> do
                putStrLn "Advancing state with A"
                loop ('A' : state)
            "B" -> do
                putStrLn "State didn't change"
                loop state
            _ -> do
                putStrLn "Finishing game"
                pure state  -- recursion ends here

So a combination of recursion and pattern-matching allows you to change your state depending on your input. So when you call loop next time, it's given a possible new state.

Upvotes: 2

Related Questions