IO behaves oddly if used nested

Alastair Reid alastair at
Sat Oct 4 19:12:16 EDT 2003

> The odd is in the conceptual explanation. If I give a description of some f
> x = y function in Haskell I expect that some program f x is reduced to y
> and the result is given back (possibly printed). A good story to sell to
> students.

This is true of IO as well.

The bit that's tripping up your students is the question of what an object of 
type 'IO Int' is.  It sound like they think it is the same as Int.  But this 
isn't so - no more than an object of type [Int] or 'Maybe Int' is the same as 
an Int.

It sounds like you're trying to make that distinction when you talk about the 
'Nomad' datatype but you have to be careful to keep on making that 
distinction every time you talk about monads and be careful to distinguish 'a 
value of type t' from 'a computation which returns a value of type t' or, at 
least, keep making that distinction until after your audience fully 
appreciates the distinction and can handle more informal explanations.

Once they get round the idea that 'IO Int /= Int', surely it is reasonable to 
accept that '1' and 'return 1' might be printed in different ways?

And if you've previously shown them that 'show id' produces a fairly 
uninformative result, maybe they can accept that 'show (do{c <- getChar; 
putChar c})' produces a similarly uninformative result?

You might also try to warm students up to the issues by having them think 
about what (incorrectly typed) expressions like:

  ord getChar + ord getChar


  (getChar, getChar)


  let f x = do{ print x; return (x+1) }
  in drop 3 [ f i | i <- [1..10] ]

might do under different evaluation orders.  Understanding the problem makes 
it clearer why we want 'IO Int' /= 'Int'.

> * Why is an IO a evaluated if I am not interested in it's result? (opposite
> to the f x = y lazy behavior)

You are interested in its result though.

If you recall my example of mailing an order for socks to a clothing company, 
evaluating a value of type 'IO Int' corresponds to opening the envelope 
containing the order.

Opening the envelope does not, in itself, lead to socks being sent.  For  
example, they may decide to discard the first 3 valid orders they receive 
each day or they may reject orders from people with a 'b' in their name.  It 
is only when they decide to execute the order than the socks get sent.

> * Why is in the putStr "hello world" example Hello World not shown?
> (opposite to expected f x = y eval-first-then-show behavior)

For the same reason that my feet get cold and ink-stained if I wear orders for 
socks instead of wearing socks.

The value 'putStr "hello world"' is a computation which will print "hello 
world" when you execute it.  Evaluating the computation is not the same as 
executing it.

> * Why is in the IO (IO ()) example the inner IO () not evaluated? (somewhat
> opposite to expected f (f x) behavior - I personally wonder if it is even
> sound in a category theoretical setting)

It _is_ evaluated.

But evaluating a value is not the same as executing it just as opening an 
order is not the same as obeying instructions in the order.

> A nice (old) idea would be to represent
> IO as programs which are interpreted by some _outside_ RTS in a given
> manner, and leave the Haskell language clean.
> (It might even be a good idea with respect to the compiler implementation
> since it removes checking against unsafe IO behavior from the compiler --
> just a thought)

Haskell used to do this prior to Haskell 1.3.
We moved away from it for two reasons:

1) It was notoriously hard to write correct code using the old interface 
because you had to reason carefully about the order of evaluation of pure 
Haskell code.

2) As specified, it gave us a fixed language in which to express commands and 
responses.  For example, there was no command to open a network connection or 
to draw a circle on a window and no response to say 'the result is this COM 
object'.  IMHO, Haskell would be an interesting but useless academic toy 
without the ability to do things like that.

The first might have been fixable (e.g., Clean provides a non-monadic 

The second might also be fixable but I doubt that any useful solution would 
avoid the cleanliness and safety issues you refer to.  The sad truth is that 
as soon as you connect to a world that is not strongly typed (and uses a 
different type system anyway) and doesn't perform any runtime checks to 
compensate, you get corrupted by the a bunch of safety issues.  

Alastair Reid

More information about the Haskell mailing list