To test my skills in Haskell, I decided I wanted to implement the very first game you find in Land of Lisp / Realm of Racket. The "Guess My Number" game. The game relies on mutable state to run, as it constantly has to update upper and lower bounds for the program to home in on the value the user is thinking of.
It goes a little something like this:
> (guess)
50
> (smaller)
25
> (bigger)
37
Now, this sort of thing (to my knowledge) isn't entirely possible in Haskell, calling some function from the REPL that modifies global mutable state, then prints the result immediately after, as it violates the principle of immutability. Therefore all of the interaction must live inside of an IO
and/or State
monad. And that's where I'm stuck.
I can't seem to be able to wrap my mind around combining the IO
monad and the State
monad, so I can get input, print results and modify state, all in the same function.
Here's what I got so far:
type Bound = (Int, Int) -- left is the lower bound, right is the upper
initial :: Bound
initial = (1, 100)
guess :: Bound -> Int
guess (l, u) = (l + u) `div` 2
smaller :: State Bound ()
smaller = do
bd@(l, _) <- get
let newUpper = max l $ pred $ guess bd
put $ (l, newUpper)
bigger :: State Bound ()
bigger = do
bd@(_, u) <- get
let newLower = min u $ succ $ guess bd
put $ (newLower, u)
All I need to do now, is to devise a way to
How do I combine IO
and State
in an elegant way to achieve this?
Note: I'm aware that this can probably be achieved without the use of state at all; but I want to make it stay true to the original
You can combine different monads using monad transformers - in this case StateT
. You can use your existing code by changing the type signatures to use StateT
:
bigger, smaller :: Monad m => StateT Bound m ()
then you can write a function to run the game given a state parameter:
game :: StateT Bound IO ()
game = do
s <- get
liftIO $ print (guess s)
verdict <- (liftIO getLine)
case verdict of
"smaller" -> smaller >> game
"bigger" -> bigger >> game
"ok" -> return ()
_ -> (liftIO $ putStrLn $ "Unknown verdict " ++ verdict) >> game
you use liftIO
to lift an IO
action into the StateT Bound IO
monad, allowing you to prompt for input and read the next line.
Finally you can run the game using runStateT
:
runStateT game initial
What you ask is sort of possible...
import Data.IORef
makeGame :: IO (IO (), IO (), IO ())
makeGame = do
bound <- newIORef (1, 100)
let guess = do
(min, max) <- readIORef bound
print $ (min + max) `div` 2
smaller = do
(min, max) <- readIORef bound
let mid = (min + max) `div` 2
writeIORef bound (min, mid)
guess
bigger = do
(min, max) <- readIORef bound
let mid = (min + max) `div` 2
writeIORef bound (mid, max)
guess
return (guess, smaller, bigger)
Nevermind how much redundancy is in that code, this was just a quick proof of concept. Here's an example session:
$ ghci guess.hs
GHCi, version 7.9.20141202: http://www.haskell.org/ghc/ :? for help
[1 of 1] Compiling Main ( guess.hs, interpreted )
Ok, modules loaded: Main.
*Main> (guess, smaller, bigger) <- makeGame
*Main> guess
50
*Main> smaller
25
*Main> bigger
37
*Main>
Nested IO
types can be fun and useful.
You don't have to use the State monad at all in this example. Here is an example that passes the state as parameter:
loop :: Bound -> IO ()
loop bd@(l,u) = do
putStr "> "
line <- getLine
case line of
"(guess)" -> print (guess bd) >> loop bd
"(smaller)" -> do
let newUpper = max l $ dec $ guess bd
print $ guess (l, newUpper)
loop (l, newUpper)
"(bigger)" -> do
let newLower = min u $ inc $ guess bd
print $ guess (newLower, u)
loop (newLower, u)
"" -> return ()
_ -> putStrLn "Can't parse input" >> loop bd
main :: IO ()
main = loop initial
Otherwise, the concept you are looking for are monad transformers. For example using StateT:
smaller :: StateT Bound IO ()
smaller = do
bd@(l, _) <- get
let newUpper = max l $ dec $ guess bd
put $ (l, newUpper)
bigger :: StateT Bound IO ()
bigger = do
bd@(_, u) <- get
let newLower = min u $ inc $ guess bd
put $ (newLower, u)
guessM :: StateT Bound IO ()
guessM = get >>= lift . print . guess
loop :: StateT Bound IO ()
loop = do
lift $ putStr "> "
line <- lift getLine
case line of
"(guess)" -> guessM >> loop
"(smaller)" -> do
smaller
guessM
loop
"(bigger)" -> do
bigger
guessM
loop
"" -> return ()
_ -> lift (putStrLn "Can't parse input") >> loop
main :: IO ()
main = evalStateT loop initial
See this chapter of Real World Haskell for a tutorial on the subject of monad transformers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With