Alberto Pellizzon
Alberto Pellizzon

Reputation: 609

How to handle IO return values properly in Haskell

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

Answers (3)

Luis Casillas
Luis Casillas

Reputation: 30227

Is there another way to parse IO String into String?

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 call isCorrect inside the do 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 the String 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:

  • A question question :: IO Question
  • An answer 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:

  1. You try to write most of your code as pure functions
  2. You use auxiliary functions like liftA2 to adapt them to work with impure effects.

It takes some study and practice to get the hang of this.

Upvotes: 0

Random Dev
Random Dev

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

Zeta
Zeta

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

Upvotes: 6

Related Questions