odd interactions (was: IO behaves oddly if used nested)

C.Reinke C.Reinke at kent.ac.uk
Mon Oct 6 20:48:25 EDT 2003

[moved to haskell-cafe]

> 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 almost everywhere the case except for the IO monad. 

indeed. Although, for the benefit of your students, you'll want to
separate printing and reduction, even in the first case.

The important thing to realise here (and to pass on to your
students) is that input/output-interactions and functional program
reductions are two conceptually very different things, and that this
difference is independent of which of the many functional I/O
systems you use. So you can not use the functional reduction
explanation for I/O as well.

Once upon a long ago, I had to add IO to a purely functional
reduction system as part of my PhD work, and to clarify things for
myself, I tried to express the various available functional I/O
systems in the common framework of transformation rules (program
reduces to program, state transforms into state). The difference
then simply became one of context-free versus context-sensitive

[summarised in chapter 3 of my old thesis


perhaps you can find some useful suggestions there?]

The differences between character streams, request/response streams,
result continuations, monadic I/O, or even uniqueness-typed
environment passing are merely differences in how the context-free
functional program reductions are embedded in a context-sensitive
environment of I/O devices. They each have their pros&cons (the
chapter tries to outline a logical development between the systems),
and they all try to embed functional reductions into their I/O
context in such a way that doing I/O does not look too foreign.

So, each of the functional I/O systems lets you play down the
difference between context-sensitive input/output and functional
reductions to some extent, but in each of the systems, you quickly
run into trouble if you try to take the similarities too far.

> 3* Hmm, feels like math, looks like math, ahah! is math!
> (designers and thinkers)

Making the distinction between context-free and context-sensitive
transformations explicit should help your group 3, as it gives them
an operational semantics view of what their programs mean, and what
they are supposed to do.

> from what they expect. The usual questions of group 3:
> * Why is an IO a evaluated if I am not interested in it's result?
> (opposite to the f x = y lazy behavior)

main is evaluated because you asked the system to run its value
(the whole IO a, not the a-typed result returned by running it).

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

implementation deficiency. a more complete implementation might
permit you to show intermediate steps in the program+device state

  program                    || device context
  putStr ("hello "++"world") || <nothing here>

-context-free reductions-->*

  putStr "hello world"       || <nothing here>

-context-sensitive interaction-->

  return ()                  || "hello world"

Just before the interaction, "hello world" is part of the program,
so a step-by-step implementation might show it (you couldn't show it
without implementation help, as the IO type is abstract), but after
the interaction, the string has moved from the program to the device
context in which it the program is running. The string appears on
some output device, the implementation could show you return () as
the final value of your program (the reduction system I was working
with did so).

> * 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)

urgh,please.. categories do not need a theory about everything [ducks quickly;]

first: in an expression of type IO (IO ()), there is not necessarily
an inner IO (). Evaluating the expression, and running the result,
will fail or return an IO (), but that might not even exist
beforehand (that's just the good old no String in IO String).

second: the inner IO () returned by running the outer IO (IO ()) is
not evaluated unless needed, and it won't be needed unless run. To
run any IO a-typed expression, the current monadic I/O system
requires it to be placed at the boundary between your functional
program and the I/O context it is running in, i.e., it has to be
part of the sequence of IO-operations that make up the value of
main. Until it gets run this way, it's just an expression that
could evaluate to an IO-script that could be run.

> * ...Lots of other questions...

better than individual answers is a simple conceptual model that
allows the students to answer such questions (and any others they
might run into). 

> Hmm, have to finish this email now - time constraints. I guess the short 
> story just is: IO monads == sometimes bad conceptual story to sell. 

no. and especially not to that group of students. but for this
group, you can't brush the difficulties under the carpet, as you can
with the other groups (and Haskell's I/O system and sugar were
designed to be easy on the first two groups). you have to give them
something more to wrap their minds around.

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

That is a valid operational semantics, if you get the interleaving
right. That it isn't the most efficient implementation shouldn't
matter in a functional programming introduction.

> (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)

Simpler implementation is often not more efficient implementation.


More information about the Haskell-Cafe mailing list