>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