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