[snip]
Neither example is odd behavior, unless you consider Hugs providing a perfectly reasonable instance of Show for IO a odd.
True, every program behaves exactly as according to the definition of the run-time behavior of Haskell programs. (Hugs deviates a bit from ghc but ah well) The odd is not in the run-time behavior. 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. In the former example, the inner putStr is _not_ evaluated but a message is shown (hinting that a IO has some structure we can observe before evaluation). In the latter example, it is shown that we are not interested in a result of a IO program, but only in its (lazily generated) side-effects. The monad behaves oddly with respect to the f x = y behavior. I think I observe the following reactions when I explain IO: 1* Can't the guy in front of me stop poking his nose? (the blank faces and parse errors) 2* Hmm, feels like Java, looks like Java, ahah! is Java! (coders) 3* Hmm, feels like math, looks like math, ahah! is math! (designers and thinkers) Group 1 will mimic, group 2 will produce obfuscuated Java code in Haskell, group 3 has great difficulty with the IO monad (although they are the first group who will write OK haskell programs) because it behaves differently 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) * Why is in the putStr "hello world" example Hello World not shown? (opposite to expected f x = y eval-first-then-show behavior) * 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) * ...Lots of other questions... Hmm, have to finish this email now - time constraints. I guess the short story just is: IO monads == sometimes bad conceptual story to sell. Especially so since the "best" group has the biggest problems and non-trivial examples are only explainable in the end in terms of run-time behavior. What was that remark again about pointy headed people and Haskell? ;-) For that I also gave the Nomad datatype as a possible solution (needs a pretty hefty type checker though). 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) Bye now, and cheers, l4t3r _________________________________________________________________ The new MSN 8: smart spam protection and 2 months FREE* http://join.msn.com/?page=features/junkmail
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 or (getChar, getChar) or 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 alternative). 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 www.haskell-consulting.com
On 20031004T181216+0100, Alastair Reid wrote:
be careful to distinguish 'a value of type t' from 'a computation which returns a value of type t'i
I always found the idea of a "computation" as a value a little hard to grasp. Therefore, when I introduced monadic transput in my functional programming course [1], I took the following approach: IO a is the type of a program with an exit value of type a; return, fail, putChar and getChar are primitive programs, and monadic transput is about combining programs using (>>=). The idea of denoting programs without executing them seems natural (in contrast to denoting computations without executing them). Likewise, it seems natural that there is a special mechanism for actually executing a program: either you give it to GHCi, or you bind it to Main.main and compile the module (thus, the program becomes synonymous with the expression that you bind to Main.main). Up until now, nobody in my class seems to have had problems with this mental model of monadic transput (of course, in grand tradition of Finnish universities, a large subset of my students are not actually attending sessions, so I wouldn't know about them until exam time). [1] Advanced-level introduction to FP; students are advanced undergrads or pre-master graduates. They have a strong background in imperattive programming, with the usual load of data structures and algorithms, operating systems, automata and formal languages theory (and other stuff depending on their tastes). (Note that printing a program is also a natural idea, and likewise natural is the idea that the output is unintelligible.) -- Antti-Juhani Kaijanaho, FM (MSc), http://www.mit.jyu.fi/antkaij/ ohjelmistotekniikan assistentti * assistant in software engineering Jyväskylän yliopisto * University of Jyväskylä Tietotekniikan laitos * Dept. of Mathematical Inf. Tech.
participants (3)
-
Alastair Reid -
Antti-Juhani Kaijanaho -
blaat blaat