
Let me know if I'm bugging you guys too much. It'd be great if I had someone close at hand who knew more about Haskell than I do, but unfortunately I don't. Are there any fora that are especially for people to help novices, or is haskell-cafe the best fit?
It's quite okay to ask such questions here, but you should not assume to be the first to run into these problems:-) Have a search through www.haskell.org, especially the bookshelf, the mailing-list archives for haskell and haskell-cafe, and the wiki: questions & answers (which is similar to a list of frequently asked questions, without the ordering implied by a list..). This particular question has also been raised several times on comp.lang.functional, so you can find a lot of related discussion in Google's UseNet search. You won't find all the answers you need there, and even if your specific questions have been answered there, the answers may not be helpful to you. Then, ask here, and say what resources you've tried and why they didn't help you. That way, the resources at www.haskell.org can be improved every time the questions are asked on this list.
...what's surprising me is that do I really have to turn everything into an IO action like this just to do things with the String hidden in the IO String?
Part of the answer can already be found in the wiki at www.haskell.org, but as you say you've tried some monad tutorials, here goes another longish explanation attempt: There is no String hidden in an IO String (at least, there need not be one). If you have a function f :: String -> String, there need not be a String hidden in f -- a call to f could just give you back something constructed from its parameter. So f promises a String when passed a String, and the only way to get at that result String is by applying f to a String. If you have i :: IO String, the situation is similar: - f is a function with an explicit parameter and calling this function returns a String - i is an IO-action, with implicit access to an IO-environment, and executing it may do things to the IO-environment, and will produce a String In both cases, you've got a promise of a String, but not necessarily a String. The difference is that f only has access to its definition and its parameter, and it only returns a String, so you can use it in any context that supplies a String parameter and expects a String result. In contrast, i also wants access to an IO-environment, and it returns a String and may modify the IO-environment, so you can use it in any context that supplies access to an IO-environment and expects a String and (potential) changes to that IO-environment. With this background, your question is easily answered: an IO String action only promises a String, and to get that String you have to execute the action in an IO-environment. You can't do that inside an expression that isn't of type IO something, because expressions that are not of that type shouldn't have access to an IO-environment (they may pass IO actions around, but they can't execute them). So, you don't need to convert everything to an IO action to do something with the String, but you need to be able to execute the IO action that promises the String. And you can't embed that IO action in a non-IO expression, so your overall programm will be an IO action:-( However, you can embed functional expressions in an IO action:-) And that's just a complicated way to describe what you've already discovered: f :: String -> Char {- no IO here, and f could be an arbitrarily complex functional computation -} f s = head s i :: IO String {- if given access to an IO-environment, this should produce a String -} i = readFile "input" (i >>= (\s-> return (f s))) >>= putChar {- or: do { s <- i; c<- return (f s); putChar c } or: do { s <- i; putChar (f s) } -} The "return" embeds an arbitrary expression into an IO-action that does not access its IO-environment (as far as that environment is concerned, it is a null action). And the "s<- i; return (f s)" part binds s to the String returned by i *and* it composes the effects that i and "return (f s)" might have on the IO-environment. That's why you can't simply use a let-binding instead of the monadic binding: let doesn't know about that extra IO-environment. Or, in monad-speak: function application and let-binding take place in the identity monad (the monad which doesn't add anything extra). IO actions and their bindings take place in the IO monad (the monad that adds access to IO-environments to functional computations). In contrast to other monads, such as List, MayBe, .., you won't find an operation of type M a -> a if M is the IO monad (guess what, you will, but it's unsafe;-). The reason is that other agents observe the IO-environment, so changes to it won't go unnoticed (you can throw away the evidence that you really had a list of results instead of just a single result, but you can't throw away the evidence that you've reached outside your functional program..). This brings us back to your idea of "tainting": not the Strings themselves are tainted (they are as pure as anything else), the computation that produces the String is tainted if it needs IO to produce a String. For "untainted", purely functional String computations, there is no difference (apart from resource usage) between the computations and the Strings they produce, but for "IO-tainted" computations, there is such a difference. Anyone still reading?-) If yes, and if it should have been helpful, perhaps someone could condense this and add it to the wiki? Claus PS. Once Upon A Long Ago, I tried to put some of the various functional IO schemes into a logical development (a kind of "design proof" or "design derivation").. a bit dated, and not necessarily helpful to those currently struggling with IO in Haskell, but perhaps of historical interest?-) Those who like that kind of thing can find it in chapter 3 of someone's thesis: http://www.cs.ukc.ac.uk/people/staff/cr3/publications/phd.html