On 20/12/05, Benjamin Franksen <benjamin.franksen at bessy.de> wrote:
> On Tuesday 20 December 2005 20:58, Peter Simons wrote:
> > Daniel Carrera writes:
> > > I'm scared of monads :) I really don't know what a monad
> > > is.
> >
> > Neither do I, but that doesn't mean that I can't use just
> > fine. ;-)
> >
> > >> putStrLn :: String -> World -> World
> > >
> > > That seems less scary.
> >
> > Things become a lot clearer when you think about how to
> > print _two_ lines with that kind of function. You'd write:
> >
> > f :: World -> World
> > f world = putStrLn "second line" (putStrLn "first line" world)
> >
> > The 'world' parameter forces the two functions into the
> > order you want, because printing "second line" needs the
> > result of printing "first line" before it can be evaluated.
> >
> > However, writing complex applications with that kind of API
> > would drive you nuts,
>> and would also be unsafe without some kind of strong guarantee that each
> single 'world' value is unique. (This is how they do it in Clean.)
> Imagine
>> g :: World -> World
> g world = let world' = putStrLn "first line" world
> in putStrLn "second line" world -- oops, forgot the "'"
>Well, this would just cause the first line to be ignored, wouldn't it?
I suppose it depends on one's perspective, and what the World type
really looks like. If you're cheating by threading a trivial value,
and getting what are supposed to be pure functions to have side
effects, then this is a problem. If World is a suitable type for
holding information about the actions to eventually perform based on
user input, then this will work just fine (though it's likely a bug
the way that it's written).
To me, this view doesn't seem any less scary than the monadic
approach. You've just moved the scary part (if any) into the World
type. I believe that a type somewhat along the lines of [Request] ->
[Response] was at one point used for this.
At some point you should really stop caring exactly how it's
implemented though, and simply regard these IO actions as the sum of
their parts. You have a bunch of primitives (getChar :: IO Char,
putChar :: Char -> IO (), etc) that describe actions to be performed,
and some operations, the monad operations on IO included, for
combining them into composite actions.
A simple example of one of these combining operations is the
sequencing operation:
(>>) :: IO a -> IO b -> IO b
If x and y are IO actions, then (x >> y) is just an IO action which
performs x, throws away any result, and then performs y, returning the
result of that. For example, (putStrLn xs) is an action which prints
the string xs on the screen, and (putStrLn "Hello" >> putStrLn
"World") will print the strings "Hello" and "World" on subsequent
lines.
What if we don't want to throw away the result of the first operation?
Well, there is a slightly more complicated version of the sequencing
operation for that:
(>>=) :: IO a -> (a -> IO b) -> IO b
Now, this might seem scary at first, but all that it is doing is
chaining actions together in sequence. The expression (x >>= f)
produces the action which executes the action x, and applies f to the
result of that action, which gives a new action that it executes in
turn.
As an example, getLine :: IO String, is an action which gets a line of
text from the user. (getLine >>= \x -> putStrLn x) is an action which
gets a line of text from the user, and then prints it back out.
By understanding the primitives and the combiners, it's possible to
understand everything there is to know about IO in Haskell. You don't
actually need to look at how it's implemented (and probably shouldn't
rely on one implementation too much). Let the RTS worry about the data
structures used to represent your actions, exposing the implementation
details just makes things more complicated.
This is not to say that understanding monads in general isn't
worthwhile, it very much is. It's also not to say that monads should
always be treated like black boxes, as most often they aren't at all
black boxes. However, it's possible for the library designer to give
you a monad whose inner workings you can't inspect directly, and in
the case of IO, that's basically what has been done. It should be
respected, because it actually simplifies matters not to worry about
the implementation.
- Cale