Note: This post is written in literate Haskell. You can save it as Game.lhs and try it in your GHCi.
now playerAnswer has type IO String
so do i have to call isCorrect inside the do block?
Yes, if you stay on that course.
Is there another way to parse IO String
into String
?
None that's not unsafe. An IO String
is something that gives you a String
, but whatever uses the String
has to stay in IO
.
In case of first i am feeling like loosing all the benefits from functional programming because i will end up writing my entire code in do blocks in order to access the String value .
This can happen if you don't take measurements early. However, let's approach this from a top-down approach. First, let's introduce some type aliases so that it's clear whether we look at a String
in as an answer or a name:
> type Text = String
> type Answer = String
> type Name = String
> type Points = Int -- points are usually integers
Your original types stay the same:
> data Question = Question { answer :: Answer
> , text :: Text } deriving Show
> data Player = Player { name :: Name
> , points :: Points } deriving Show
Now let's think about a single turn of the game. You want to ask the player a question, get his answer, and if he's right, add some points:
> gameTurn :: Question -> Player -> IO Player
> gameTurn q p = do
> askQuestion q
> a <- getAnswer
> increasePointsIfCorrect q p a
This will be enough to fill your game with a single turn. Let's fill those functions with life. askQuestions
and getAnswer
change the world: they print something on the terminal and ask for user input. They have to be in IO
at some point:
> askQuestion :: Question -> IO ()
> askQuestion q = putStrLn (text q)
> getAnswer :: IO String
> getAnswer = getLine
Before we actually define increasePointsIfCorrect
, let's think about a version that does not use IO
, again, in a slightly more abstract way:
> increasePointsIfCorrect' :: Question -> Player -> Answer -> Player
> increasePointsIfCorrect' q p a =
> if isCorrect q a
> then increasePoints p
> else p
By the way, if you watch closely, you'll notice that increasePointsIfCorrect'
is actually a single game turn. After all, it's checks the answer and increases the points. Speaking of:
> increasePoints :: Player -> Player
> increasePoints (Player n p) = Player n (p + 1)
> isCorrect :: Question -> Answer -> Bool
> isCorrect q a = answer q == a
We now defined several functions that don't use IO
. All that's missing is increasePointsIfCorrect
:
> increasePointsIfCorrect :: Question -> Player -> Answer -> IO Player
> increasePointsIfCorrect q p a = return (increasePointsIfCorrect' q p a)
You can check this now with a simple short game:
> theQuestion = Question { text = "What is your favourite programming language?"
> , answer = "Haskell (soon)"}
> thePlayer = Player { name = "Alberto Pellizzon"
> , points = 306 }
>
> main :: IO ()
> main = gameTurn theQuestion thePlayer >>= print
There are other ways to handle this, but I guess this is one of the easier ones for beginners.
Either way, what's nice is that we could now test all the logic without using IO
. For example:
prop_increasesPointsOnCorrectAnswer q p =
increasePointsIfCorrect' q p (answer q) === increasePoints p
prop_doesnChangePointsOnWrongAnswer q p a = a /= answer q ==>
increasePointsIfCorrect' q p a === p
ghci> quickCheck prop_increasesPointsOnCorrectAnswer
OK. Passed 100 tests.
ghci> quickCheck prop_doesnChangePointsOnWrongAnswer
OK. Passed 100 tests.
Implementing those tests completely is out of scope of this question though.
Exercises
- Tell the player whether his answer was correct.
- Add
playGame :: [Question] -> Player -> IO ()
, which asks several questions after another and tells the player the final score.
- Ask the player for his/her name and store it in the initial player.
- (Very Hard for a beginner) Try to find a way so that you can either play a game automatically (for example for testing), or "against" a human. Hint: Look for "domain specific language".