Why IO Input Types are Confusing

module WhyNoIOinput where

import System.Random

Our toy example for this note: We want to generate two random numbers and then output their sum and product. We learned before that random stuff in Haskell needs this scary thing called IO monad, so we expect this to be difficult.

First we need something to generate random numbers:

makeInt :: IO Int
makeInt = randomRIO (1,10) :: IO Int

Now, let's add and multiply them.

sumIO :: IO Int -> IO Int -> IO Int
sumIO makeX makeY = do
  x <- makeX
  y <- makeY
  return (x+y)

productIO :: IO Int -> IO Int -> IO Int
productIO makeX makeY = do
  x <- makeX
  y <- makeY
  return (x*y)
sumAndProductIO :: IO Int -> IO Int -> IO (Int,Int)
sumAndProductIO makeX makeY = do
  s <- sumIO makeX makeY
  p <- productIO makeX makeY
  return (s,p)
example1 :: IO ()
example1 = do
  (s,p) <- sumAndProductIO makeInt makeInt
  putStrLn $ "Sum:" ++ show s
  putStrLn $ "Product:" ++ show p

So, what do we get?

*WhyNoIOinput> example1
*WhyNoIOinput> example1

But wait, if 21 = x * y, then we can never have x + y = 13.

So, what is going wrong here?

The problem is that makeInt is not evaluated inside example1. And also the sumAndProductIO function does not run it to get concrete values but is just handing it over to sumIO and productIO.

Imagine I have a calculator with a random number button. Then I can push the button and then tell you the result. Or I can give you the calculator and you push the button yourself. The difference seems unimportant and indeed it is irrelevant if we are only interested in you getting to know one value. But if I also want to give a random number to someone else then it matters: Do I push the button and then give you and your friend the same number or do I give the same calculator, i.e. merely the same way to generate a number to both of you and you push the buttons which can result in different numbers.

We can see what is happening if we use a function that always shows the random numbers it generates:

makeIntVerbose :: IO Int
makeIntVerbose = do
 x <- randomRIO (1,10)
 putStrLn $ "generated a random number: " ++ show x
 return x
example2 :: IO ()
example2 = do
  (s,p) <- sumAndProductIO makeIntVerbose makeIntVerbose
  putStrLn $ "Sum:" ++ show s
  putStrLn $ "Product:" ++ show p

This gives us:

*WhyNoIOinput> example2
generated a random number: 3
generated a random number: 1
generated a random number: 10
generated a random number: 7
*WhyNoIOinput> example2
generated a random number: 4
generated a random number: 9
generated a random number: 10
generated a random number: 2

Now it becomes clear that instead of two we always generate four random numbers. Then we get the sum of the first two and the product of the latter two.

Lesson learned: We often do not want IO Int as an input type.

So, what is the proper way to do it?

First, realise that many of your functions are actually pure!

sumAndProduct :: Int -> Int -> (Int,Int)
sumAndProduct x y = (x+y,x*y)

Use a wrapper function which collects the results of IO functions and then feeds them into the pure function and returns the result.

randomSumAndProduct :: IO (Int,Int)
randomSumAndProduct = do
  x <- makeInt
  y <- makeInt
  let sap = sumAndProduct x y
  return sap

This gives:

*WhyNoIOinput> randomSumAndProduct
*WhyNoIOinput> randomSumAndProduct

Try replacing makeInt with makeIntVerbose.

Final note: Make sure you understand the difference between x <- makeInt and let x = makeInt. In the latter variant, x will still be a way to get a value and not a concrete single fixed value!