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

[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 transformations. [summarised in chapter 3 of my old thesis http://www.cs.kent.ac.uk/people/staff/cr3/publications/phd.html 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 transformation: 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. Hth, Claus

I'd just like to add a brief note to what Claus has said: On 2003-10-06 at 19:48BST "C.Reinke" wrote:
[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.
I think one of the causes of confusion here is that the interpreters (ghci and hugs) do something solely for the convenience of the user -- namely print out /values/ -- that has nothing to do with the execution of Haskell /programmes/. I don't think that the definition of the language has anything to say about the effect of evaluating an expression. You could imagine an interpreter that only permitted objects of type IO () (or IO t, but that would still be confusing) to be entered at the top level. It would be less useful, because it would respond to an input of 2+2 with a type error and you'd have to enter print $ 2+2 instead. Alternatively it might respond to 2+2 with "it:: Integer" and then you could enter "print it". Either of these would be less convenient to use, but would avoid that "(possibly printed)" above. If you remove the "(possibly printed)" and tweak a bit you get
[...] If I give a description of some f x = y function in Haskell I expect that some /expression/ f x is reduced to y and the result is given back. A good story to sell to students.
And I'd agree completely, but you can't output a value to a terminal, you have to output a sequence of characters, and to do that you have to observe certain conventions that have nothing to do with the value itself. Cheers, Jón -- Jón Fairbairn Jon.Fairbairn@cl.cam.ac.uk
participants (2)
-
C.Reinke
-
Jon Fairbairn