Is the monadic IO construct in Haskell just a convention?

haskell

Solution

Yes, monadic I/O is a consequence of Haskell being lazy. Specifically, though, monadic I/O is a consequence of Haskell being pure, which is effectively necessary for a lazy language to be predictable.†

This is easy to illustrate with an example. Imagine for a moment that Haskell were not pure, but it was still lazy. Instead of `putStrLn` having the type `String -> IO ()`, it would simply have the type `String -> ()`, and it would print a string to stdout as a side-effect. The trouble with this is that this would only happen when `putStrLn` is actually called, and in a lazy language, functions are only called when their results are needed.

Here’s the trouble: `putStrLn` produces `()`. Looking at a value of type `()` is useless, because `()` means “boring”. That means that this program would do what you expect:

main :: ()
main =
  case putStr "Hello, " of
    () -> putStrLn " world!"

-- prints “Hello, world!\n”

But I think you can agree that programming style is pretty odd. The `case ... of` is necessary, however, because it forces the evaluation of the call to `putStr` by matching against `()`. If you tweak the program slightly:

main :: ()
main =
  case putStr "Hello, " of
    _ -> putStrLn " world!"

…now it only prints `world!\n`, and the first call isn’t evaluated at all.

This actually gets even worse, though, because it becomes even harder to predict as soon as you start trying to do any actual programming. Consider this program:

printAndAdd :: String -> Integer -> Integer -> Integer
printAndAdd msg x y = putStrLn msg `seq` (x + y)

main :: ()
main =
  let x = printAndAdd "first" 1 2
      y = printAndAdd "second" 3 4
  in (y + x) `seq` ()

Does this program print out `first\nsecond\n` or `second\nfirst\n`? Without knowing the order in which `(+)` evaluates its arguments, we don’t know. And in Haskell, evaluation order isn’t even always well-defined, so it’s entirely possible that the order in which the two effects are executed is actually completely impossible to determine!

This problem doesn’t arise in strict languages with a well-defined evaluation order, but in a lazy language like Haskell, we need some additional structure to ensure side-effects are (a) actually evaluated and (b) executed in the correct order. Monads happen to be an interface that elegantly provide the necessary structure to enforce that order.

Why is that? And how is that even possible? Well, the monadic interface provides a notion of data dependency in the signature for `>>=`, which enforces a well-defined evaluation order. Haskell’s implementation of `IO` is “magic”, in the sense that it’s implemented in the runtime, but the choice of the monadic interface is far from arbitrary. It seems to be a fairly good way to encode the notion of sequential actions in a pure language, and it makes it possible for Haskell to be lazy and referentially transparent without sacrificing predictable sequencing of effects.

It’s worth noting that monads are not the only way to encode side-effects in a pure way—in fact, historically, they’re not even the only way Haskell handled side-effects. Don’t be misled into thinking that monads are only for I/O (they’re not), only useful in a lazy language (they’re plenty useful to maintain purity even in a strict language), only useful in a pure language (many things are useful monads that aren’t just for enforcing purity), or that you needs monads to do I/O (you don’t). They do seem to have worked out pretty well in Haskell for those purposes, though.

† Regarding this, Simon Peyton Jones once noted that “Laziness keeps you honest” with respect to purity.

Problem

Regarding Haskell's monadic `IO` construct: Is it just a convention or is there is a implementation reason for it? Could you not just FFI into libc.so instead to do your I/O, and skip the whole `IO`-monad thing? Would it work anyway, or is the outcome nondeterministic because of: (a) Haskell's lazy evaluation? (b) another reason, like that the GHC is pattern-matching for the `IO` monad and then handling it in a special way (or something else)? What is the real reason - in the end you end up using a side effect, so why not do it the simple way?

Original source

Related problems