Reputation: 609
Hi i have a very noob question, lets say i want to create a game when you have to answer questions, i wrote this
data Question = Question { answer::String, text::String }
data Player = Player { name::String, points::String }
answerQuestion :: Question -> Player -> Player
answerQuestion question player
| isCorrect question playerAnswer = Player (name player) (points player + 1)
| otherwise = player
where
playerAnswer = do
putStrLn text(question)
getLine
isCorrect :: Question -> String -> Bool
isCorrect question try = try == answer(question)
now playerAnswer has type IO String
so do i have to call isCorrect
inside the do
block ? Is there another way to parse IO String
into String
?
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
Upvotes: 1
Views: 152
Reputation: 30227
Is there another way to parse
IO String
intoString
?
No, there is no safe way of turning IO String
into String
. This is the point of the IO
type!
now playerAnswer has type
IO String
so do i have to callisCorrect
inside thedo
block ? [...] 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 theString
value
That's what it looks like at first, but it's not like that. The trick is that we use adapter functions to connect the pure and the effectful worlds. So for example, suppose you have:
question :: IO Question
answer :: IO String
And you want to call isCorrect :: Question -> String -> Bool
on the Question
and the String
. One way to do this is to use the liftA2
function:
import Control.Applicative (liftA2)
example :: IO Bool
example = liftA2 isCorrect question answer
where question :: IO Question
question = _
answer :: IO String
answer = _
liftA2
has this generic type:
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
In this example, we're using it with:
f := IO
a := Question
b := String
c := Bool
So what liftA2 does is adapt a pure function to work with side-effecting types. And that's how Haskell programming generally works:
liftA2
to adapt them to work with impure effects.It takes some study and practice to get the hang of this.
Upvotes: 0
Reputation: 52280
as an alternative you can promote answerQuestion
to an action as well:
answerQuestion :: Question -> Player -> IO Player
answerQuestion question player =
answer <- playerAnswer
if isCorrect question answer
then return $ Player (name player) (points player + 1)
else return player
where
playerAnswer = do
putStrLn $ text question
getLine
so yes you could say that in this case you should call isCorrect
from inside a do
block
Upvotes: 1
Reputation: 105886
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
intoString
?
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.
playGame :: [Question] -> Player -> IO ()
, which asks several questions after another and tells the player the final score. Upvotes: 6