40

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?

atravers
  • 455
  • 4
  • 8
Konrad Eisele
  • 3,088
  • 20
  • 35
  • 11
    You might find [this question on the I/O model Haskell used prior to the introduction of monadic I/O](https://stackoverflow.com/q/17002119/465378) interesting. – Alexis King Jul 17 '17 at 05:22
  • Possible duplicate of [Why are side-effects modeled as monads in Haskell?](https://stackoverflow.com/q/2488646/1048572) – Bergi Jul 17 '17 at 18:14
  • You certainly *can* FFI into libc without IO. The outcome *will* be unexpected or nondeterministic. "Why not do it the simple way?" is because the *entire point* of Haskell is that you can't do it the simple way - if you allow side effects the simple way, then Haskell is just another imperative language with different syntax (similar to C or Python). – user253751 Jul 18 '17 at 00:49

5 Answers5

71

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.

Alexis King
  • 43,109
  • 15
  • 131
  • 205
  • Ok I (think I) understand: The important part is the >>= operator that forms a chain of lambda bindings that are then not optimized out and need to be called sequentially. Some things I wonder however: If instead of just "main" being the function that is forced to be evaluated, all function that lead to external sideeffect calls would be marked and forced to evaluate then monadic IO would be not needed? (except for the ordering part, but you could define the order to be the source order in an imaginary Haskell). – Konrad Eisele Jul 17 '17 at 08:41
  • Another thing that is unclear to me is the meaning of "pure": if it means there are no sideeffects, well, they are there. – Konrad Eisele Jul 17 '17 at 08:44
  • Hey Alexis, Great explanation, I never quite understood how Haskell's purity is dependent on it's laziness, that makes a lot of sense. Though, when I try running the examples you supplied in your answer I run into a lot of compiler errors: $ runhaskell helloworld.hs helloworld.hs:2:1: error: • Couldn't match expected type ‘IO t0’ with actual type ‘()’ • In the expression: main When checking the type of the IO action ‘main’ amongst other errors. – Alphonse23 Jul 17 '17 at 10:22
  • 4
    I have to (slightly) disagree with the association monadic IO <-> laziness. Theoretically, a lazy language could be impure and allow side effects. There's nothing inherently "wrong" in that, or at least it is as wrong as an eager impure language. The main issue, though, is that - as you nicely show - the evaluation order becomes very important. Even if we fix it in the semantics (sacrificing many optimizations), it is hard for humans to predict the order of the side effects. Hence, the language becomes practically unusable. This is probably what you mean with "effectively necessary", I guess. – chi Jul 17 '17 at 10:33
  • 2
    Further, I think you should add a note clarifying that in an eager language, monadic IO would make a lot of sense too. While side effects in an eager impure world are more human-understandable, a pure eager language could still choose to use monadic IO to maintain purity. – chi Jul 17 '17 at 10:36
  • 2
    @Konrad The side effects are, in a sense, not apparent from the code. You can see a value in the IO monad just as a pure value, and indeed, if a function produces such a value, you evaluate thay value, but then don't throw it into main, the side effects are not executed. You can see main as returning a sequence of actions for the runtime to execute, which it wil then do. Main is still totally pure. — also, you may want to look at the Writer monad, which does essentially the same thing, but that one even *looks* pure. – tomsmeding Jul 17 '17 at 12:24
  • 1
    Splitting hairs a little: monadic IO is not a consequence of purity, in that there are other pure languages which feature a different approach to IO. Clean notably uses a linear type, not a monad, to make sure functions which perform IO do so in a definite order. One can argue over whether monadic IO is the best design but it's not the only one. – Benjamin Hodgson Jul 17 '17 at 14:46
  • @tomsmeding I can understand that if only output and simple input that doesnt change the control path or is not used for calcuation is involved. However if you have input and then decide according to that input, my understanding is that you cannot defer this to an external engine as a seqeuence of actions, unless you calculate both control paths concurrently or if the input is used for a calculation, calcuate all possible outcomes in advance or by passing a language to the external engine. I'm not a functional expert though. – Konrad Eisele Jul 17 '17 at 16:26
  • append: If you take putStrLn (maybe bad example though because it is output) and travel downto ghc-8.0.2/libraries/base/GHC/IO/Handle/Text.hs (hPutcBuffered) you maybe can say: Haskell is perfectly "pure" up until line 496 is reached. After that all functions that are in the call path get tainted. – Konrad Eisele Jul 17 '17 at 16:29
  • 2
    @Alphonse23, The examples are not valid Haskell, they are demonstrations of an imaginary dialect of Haskell where IO was not monadic. – Paul Johnson Jul 17 '17 at 17:30
  • 2
    @chi I added some clarifications regarding both of your points. And yes, I would say that the *whole point* of this answer, or at least most of it, is to demonstrate why impurity in a lazy language would be hard to reason about. – Alexis King Jul 17 '17 at 18:01
  • 2
    @BenjaminHodgson Yes, the other answer I linked towards the end (which I wrote!) mentions that, but I edited this answer to note it explicitly. – Alexis King Jul 17 '17 at 18:02
  • 2
    @KonradEisele It depends on how you look at things. For example, consider [this answer, which describes how `IO` actions are really completely pure to evaluate](https://stackoverflow.com/a/41994672/465378), which hints at the notion that, from a certain point of view, Haskell **really is** pure, even including `IO`! The trick is that an `IO` action is a description of a side-effectful computation to execute, which the GHC runtime interprets. Of course, another interpretation is that `IO` is a side-effectful, imperative, embedded DSL. Sometimes it’s useful to consider both interpretations. – Alexis King Jul 17 '17 at 18:06
  • 1
    @KonradEisele Re: "If instead of just "main" being the function that is forced to be evaluated, all function that lead to external sideeffect calls would be marked and forced to evaluate then monadic IO would be not needed?" Okay, so instead of `getLine` returning a different *type* than `String`, it just returns `String` but has an extra "marker" indicating that it's not pure. We'll need that you can't call impure functions from normal ones. Now lets imagine the marker is spelled `IO`... I don't have space in a comment to get into it in full, but it works out exactly the same in the end. – Ben Jul 18 '17 at 03:02
25

Could you just FFI into libc.so instead to do IO and skip the IO Monad thing?

Taking from https://en.wikibooks.org/wiki/Haskell/FFI#Impure_C_Functions, if you declare an FFI function as pure (so, with no reference to IO), then

GHC sees no point in calculating twice the result of a pure function

which means the the result of the function call is effectively cached. For example, a program where a foreign impure pseudo-random number generator is declared to return a CUInt

{-# LANGUAGE ForeignFunctionInterface #-}

import Foreign
import Foreign.C.Types

foreign import ccall unsafe "stdlib.h rand"
  c_rand :: CUInt

main = putStrLn (show c_rand) >> putStrLn (show c_rand)

returns the same thing every call, at least on my compiler/system:

16807
16807

If we change the declaration to return a IO CUInt

{-# LANGUAGE ForeignFunctionInterface #-}

import Foreign
import Foreign.C.Types

foreign import ccall unsafe "stdlib.h rand"
  c_rand :: IO CUInt

main = c_rand >>= putStrLn . show >> c_rand >>= putStrLn . show

then this results in (probably) a different number returned each call, since the compiler knows it's impure:

16807
282475249

So you're back to having to use IO for the calls to the standard libraries anyway.

Michal Charemza
  • 25,940
  • 14
  • 98
  • 165
  • I highly doubt this is actually special cased by the runtime, aside from performance details. What c_rand produces here is not an actual CUInt contained in some sort of wrapper, it is a description to the runtime of what to do (in this case, to call that C function and get the result). I'd wager GHC still executes that function only once, but the difference is that that execution doesn't give the result yet. (If I'm incorrect, please tell me!) – tomsmeding Jul 17 '17 at 12:30
  • 2
    @tomsmeding, outside of the IO monad the compiler is free to call c_rand once and reuse the result or to call it for each invocation, so the result is implementation-dependent. If c_rand were pure, and hence returned the same result every time, this would not make any difference to the results, but because it is impure the optimisation makes a difference. – Paul Johnson Jul 17 '17 at 17:33
  • @Paul Ah, that's apparently a specific of the FFI specification that I didn't know of. Thanks! – tomsmeding Jul 17 '17 at 20:24
  • @tomsmeding Sure, but then the resulting action gets executed twice, which causes the C function to get called twice, which is what matters. – user253751 Jul 27 '17 at 02:31
13

Let's say using FFI we defined a function

c_write :: String -> ()

which lies about its purity, in that whenever its result is forced it prints the string. So that we don't run into the caching problems in Michal's answer, we can define these functions to take an extra () argument.

c_write :: String -> () -> ()
c_rand :: () -> CUInt

On an implementation level this will work as long as CSE is not too aggressive (which it is not in GHC because that can lead to unexpected memory leaks, it turns out). Now that we have things defined this way, there are many awkward usage questions that Alexis points out—but we can solve them using a monad:

newtype IO a = IO { runIO :: () -> a }

instance Monad IO where
    return = IO . const
    m >>= f = IO $ \() -> let x = runIO m () in x `seq` f x

rand :: IO CUInt
rand = IO c_rand

Basically, we just stuff all of Alexis's awkward usage questions into a monad, and as long as we use the monadic interface, everything stays predictable. In this sense IO is just a convention—because we can implement it in Haskell there is nothing fundamental about it.

That's from the operational vantage point.

On the other hand, Haskell's semantics in the report are specified using denotational semantics alone. And, in my opinion, the fact that Haskell has a precise denotational semantics is one of the most beautiful and useful qualities of the language, allowing me a precise framework to think about abstractions and thus manage complexity with precision. And while the usual abstract IO monad has no accepted denotational semantics (to the lament of some of us), it is at least conceivable that we could create a denotational model for it, thus preserving some of the benefits of Haskell's denotational model. However, the form of I/O we have just given is completely incompatible with Haskell's denotational semantics.

Simply put, there are only supposed to be two distinguishable values (modulo fatal error messages) of type (): () and ⊥. If we treat FFI as the fundamentals of I/O and use the IO monad only "as a convention", then we effectively add a jillion values to every type—to continue having a denotational semantics, every value must be adjoined with the possibility of performing I/O prior to its evaluation, and with the extra complexity this introduces, we essentially lose all our ability to consider any two distinct programs equivalent except in the most trivial cases—that is, we lose our ability to refactor.

Of course, because of unsafePerformIO this is already technically the case, and advanced Haskell programmers do need to think about the operational semantics as well. But most of the time, including when working with I/O, we can forget about all that and refactor with confidence, precisely because we have learned that when we use unsafePerformIO, we must be very careful to ensure it plays nicely, that it still affords us as much denotational reasoning as possible. If a function has unsafePerformIO, I automatically give it 5 or 10 times more attention than regular functions, because I need to understand the valid patterns of use (usually the type signature tells me everything I need to know), I need to think about caching and race conditions, I need to think about how deep I need to force its results, etc. It's awful[1]. The same care would be necessary of FFI I/O.

In conclusion: yes it's a convention, but if you don't follow it then we can't have nice things.

[1] Well actually I think it's pretty fun, but it's surely not practical to think about all those complexities all the time.

luqui
  • 59,485
  • 12
  • 145
  • 204
  • 1
    Interesting answer, adds a different angle to Alexis answer. If I could I would flag both as answers. Denotational Semantics is something I will need a while to understand. – Konrad Eisele Jul 17 '17 at 16:40
4

That depends on what the meaning of "is" is—or at least what the meaning of "convention" is.

If a "convention" means "the way things are usually done" or "an agreement among parties covering a particular matter" then it is easy to give a boring answer: yes, the IO monad is a convention. It is the way the designers of the language agreed to handle IO operations and the way that users of the language usually perform IO operations.

If we are allowed to choose a more interesting definition of "convention" then we can get a more interesting answer. If a "convention" is a discipline imposed on a language by its users in order to achieve a particular goal without assistance from the language itself, then the answer is no: the IO monad is the opposite of a convention. It is a discipline enforced by the language that assists its users in constructing and reasoning about programs.

The purpose of the IO type is to create a clear distinction between the types of "pure" values and the types of values which require execution by the runtime system to generate a meaningful result. The Haskell type system enforces this strict separation, preventing a user from (say) creating a value of type Int which launches the proverbial missiles. This is not a convention in the second sense: its entire goal is to move the discipline required to perform side effects in a safe and consistent way from the user and onto the language and its compiler.

Could you just FFI into libc.so instead to do IO and skip the IO Monad thing?

It is, of course, possible to do IO without an IO monad: see almost every other extant programming language.

Would it work anyway or is the outcome undeterministic because of Haskell evaluating lazy or something else, like that the GHC is pattern matching for IO Monad and then handling it in a special way or something else.

There is no such thing as a free lunch. If Haskell allowed any value to require execution involving IO then it would have to lose other things that we value. The most important of these is probably referential transparency: if myInt could sometimes be 1 and sometimes be 5 depending on external factors then we would lose most of our ability to reason about our programs in a rigorous way (known as equational reasoning).

Laziness was mentioned in other answers, but the issue with laziness would specifically be that sharing would no longer be safe. If x in let x = someExpensiveComputationOf y in x * x was not referentially transparent, GHC would not be able to share the work and would have to compute it twice.

What is the real reason?

Without the strict separation of effectful values from non-effectful values provided by IO and enforced by the compiler, Haskell would effectively cease to be Haskell. There are plenty of languages that don't enforce this discipline. It would be nice to have at least one around that does.

In the end you end you endup in a sideeffect. So why not do it the simple way?

Yes, in the end your program is represented by a value called main with an IO type. But the question isn't where you end up, it's where you start: If you start by being able to differentiate between effectful and non-effectful values in a rigorous way then you gain a lot of advantages when constructing that program.

Rein Henrichs
  • 15,437
  • 1
  • 45
  • 55
0

What is the real reason - in the end you end up using a side effect, so why not do it the simple way?

...you mean like Standard ML? Well, there's a price to pay - instead of being able to write:

any :: (a -> Bool) -> [a] -> Bool
any p = or . map p

you would have to type out this:

any :: (a -> Bool) -> [a] -> Bool
any p []     = False
any p (y:ys) = y || any p ys

Could you not just FFI into libc.so instead to do your I/O, and skip the whole IO-monad thing?

Let's rephrase the question:

Could you not just do I/O like Standard ML, and skip the whole IO-monad thing?

...because that's effectively what you would be trying to do. Why "trying"?

  • SML is strict, and relies on sytactic ordering to specify the order of evaluation everywhere;

  • Haskell is non-strict, and relies on data dependencies to specify the order of evaluation for certain expressions e.g. I/O actions.

So:

Would it work anyway, or is the outcome nondeterministic because of:

(a) Haskell's lazy evaluation?

(a) - the combination of non-strict semantics and visible effects is generally useless. For an amusing exhibition of just how useless this combination can be, watch this presentation by Erik Meiyer (the slides can be found here).

atravers
  • 455
  • 4
  • 8