Live data from Hacker News

IO Monad Considered Harmful

blog.jle.im

31–40 of 75 posts

Re: IO Monad Considered Harmful

#31
post #14

The thing that makes an 'IO t' data structure different from some arbitrary java bytecode, is that bytecode is an opaque black box, and represents commands that will be executed immediately, whereas an 'IO t' just "describes" the intended action, remains composable and is a first-class value (i.e. data structure ) in your program. It's necessary to draw a distinction between the "runtime" and "evaluation" in Haskell;…

IO t is a black box too. If it's a data structure, it's a crappy one: you can't pick it apart like a list, inspect it, or do anything except sequence it. I didn't follow what you meant about "runtime vs evaluation." To me they seem hopelessly intertwined: if you evaluate head [], you get a runtime error.

You can build a completely pure version of the IO type assuming Haskell's evaluation semantics, so long as you have one base "escape" function ffi : String -> String (or whatever serialization you want) that serves as a foreign function interface. Mind yo, the ffi function is evaluated the same way (with all the non-strictness involved) as other functions in Haskell.

There's no such thing as inpure Haskell, even in your IO monad (this is more theory than fact with GHC, for performance reasons). Basically your IO type is some instructions for the runtime. The runtime does stuff and then injects values back into Haskell, but for the language itself, values are not changing.

I have a hard time explaining it. But I think GP's point was that you evaluate main to get something of type IO (), and the runtime takes something of type IO () to do stuff with it (much like the difference between javac and java). There's some juggling going along with it...

Re: IO Monad Considered Harmful

#32

I've been writing Haskell code for years, and this is one of the most singularly useful articles I've read on it. I've certainly found the proliferation of "monad tutorials" and "learn this so you can print something" guides obnoxious, but it never occurred to me that IO really could be taught completely separately from monads. Here's the outline of what the middle of a tutorial could look like, following this advice…

I think your suggestion is a relative improvement over most tutorials. But it's still unmanageably hard. "Here's how to read something and then print that: >>=". Sure, like `getLine >>= putStrLn`. Technically speaking, that's correct. But this doesn't generalize at all. Maybe I want to greet the user by name. How do I read the name, prepend "Hello, " to it, and then print that? This is a totally natural thing to want…

> getLine >>= (return . (++) "Hello, ") >>= putStrLn

And my coworkers yesterday were complaining that ClojureScript looked like Brainfuck...

(http://en.wikipedia.org/wiki/Brainfuck)

Re: IO Monad Considered Harmful

#34
post #30

Earlier quoted context omitted.

> Here's how I might write that today, without using do notation: `getLine >>= (return . (++) "Hello, ") >>= putStrLn`. Don't write point-free code here. Here's a fairly simple version: getLine >>= \name -> putStrLn ("Hello, " ++ name) The only novel thing there is the >>= operator itself. You don't even have to introduce return there; you can introduce return later.

Which can then be made to look exactly like the getLine >>= putStrLn case by doing greet name = putStrLn ("Hello, " ++ name) main = getLine >>= greet (Edited to include the definition of main)

This is a nice approach. Thanks to all of you in this thread. I think I'll try something like this when I introduce Haskell I/O in my Programming Languages class in a month or two.

Re: IO Monad Considered Harmful

#35

I read the article, and it annoyed me, because the rebuttal is obvious and wasn't addressed. The rebuttal is do-notation. The second program you write after Hello World is going to use two IO actions instead of one, and so you need a way to sequence them, and every tutorial is going to do that with do-notation. And suddenly all of the other syntax you learned, like how to declare a variable with let-in, or how arrows…

yea, but the struggle isn't really quite with monads, but with syntax. properly understanding the syntax for do notation with use in IO does not require an understanding of monads.

Re: IO Monad Considered Harmful

#36
post #7
post #2

I wish the author had explained what the difference between using the IO type and using the IO type via the monadic interface looked like. As a Haskell beginner, this article just makes me more confused. Can someone share an example or clarify?

(Also a Haskell beginner) I think his point is that the IO type may be an example of a monad, but shut up about the monad part because it's not relevant to learning how to use it.

Exactly. You only need to know about some primitive operations (like putStrLn and getLine), and ways to combine them (like >>, >>=, and return).

This was my point when I wrote my "IO tutorial" (not a monad tutorial!): http://blog.raek.se/2012/10/19/haskell-io-in-five-minutes/

Re: IO Monad Considered Harmful

#37
This is a fair reflection on one of the biggest barriers to newbs learning Haskell (which I've tried to do a couple of times and failed, in part because the syntax is weird and poorly explained, in part because the tutorials suck, and mostly because I'm not very smart, or so I'm told.)

The Haskell newb gets told two things very early on:

1) Haskell is pure pure pure!

2) Haskell can do IO via monads!

These statements are contradictory. Haskell is either pure, or can do IO (via monads or anything else.) No pure language can do IO. Period. End of story. Any claim to the contrary is simply, utterly, completely false. I don't care how much you wish it was true, or think it's possible to finesse the issue. You can't. And the worst thing you can possibly do is introduce a newb to the new language via a contradiction.

It's like idiots who introduce "negative resistance" with the up-front claim that a device has "negative resistance" without first introducing the generalized concept of resistance that the notion of "negative generalized resistance" depends on. Ordinary, unqualified "resistance" means V/I, not dV/dI, and if you use the unqualified term "resistance" in any other way you are deliberately and with malice aforethought creating confusion. Same goes for "negative temperature", doubled.

Want to teach Haskell? Start like this: "Haskell is an effectively pure language that hides side-effects behind opaque interfaces that return new objects that represent the modified state of the world, rather than old objects that have been modified. This trickery lets us reason about Haskell code in most cases as if it was pure even though it is not."

Claiming purity where there is none is terrible pedagogy. The newb will be confused and have a much harder time getting their head around the language. They will realize that the insiders are working in a different conceptual universe than they are, but won't see any way into that universe and will rightly assume that the insiders are being deliberately obtuse for the sole purpose of excluding newbs.

I am, as stated above, extremely stupid. As such, I am pretty ordinary in this regard. If you want people to learn the damned language, you have to cater to stupid people like me. So discard, play-down, reject and ignore the myth of purity, because that's what it is. A myth. Haskell is not pure. In a pure language the return value of a function depends only on its arguments, but any function that does IO can return any value whatsoever, regardless of what its arguments might be.

I'm sure Haskell purists will respond to this insisting that no, Haskell is really, truly, purely pure... for a certain definition of "purity". It isn't. Monads are a hack to cover necessary impurities. They are a good hack, but to redefine "pure" in such a way that accommodates monads--and this is what you have to do--is just like redefining "resistance" to accommodate "negative resistance". It's perfectly reasonable to generalize the definition of common terms, and perfectly confusing to use the generalization to imply that you mean "just perfectly ordinary resistance" when talking about "negative resistance," or "just perfectly ordinary purity" when talking about "purity with side effects hidden behind monads."

There is no particular value, use or virtue in claiming Haskell is "a truly pure functional language". Claiming the language is "effectively pure for most practical purposes, so you get the provability of pure functional programming by hiding side effects behind monads" seems far more useful. Why the Haskell community is so wedded to the myth of purity--and therefore so insistent about putting monads front and center, as the article rightly decries--is not clear. But the language and the community would be better for putting the myth and the monads behind them.

Re: IO Monad Considered Harmful

#38
post #9

Here's how I like to explain Monads to people. I've been told it's a decent explanation. (It ignores the monad laws and such, but it's a decent conceptual overview.) OK, so you know Java interfaces? Haskell has something just like that. They're called "type classes", though. But it works pretty much the same way. When you write out a class definition, you write A) the name of the class B) all the functions the class…

Can you add some examples of how bind and return get defined for commonly used monads, like IO? I'd love to have a more intuitive grasp of what they're for.

IO is a bad example; its instance of Monad normally exists in native non-Haskell code (or in Haskell-runtime-specific code) rather than in Haskell. Another good reason that talking about the "IO monad" early on doesn't work out well. So, I'll start with a couple of non-IO examples and then provide a suggestion for how IO works.

Let's start with Maybe. The Maybe type looks like this:

    data Maybe a = Just a | Nothing
You often use Maybe to represent functions that only handle some inputs:

    safeHead :: [a] -> Maybe a
    safeHead (x:_) = Just x
    safeHead _ = Nothing

    > safeHead [1,2,3]
    Just 1
    > safeHead []
    Nothing
The Maybe monad lets you assemble a series of operations of type Maybe into a single compound function of type Maybe. If any of the individual operations produce Nothing, the whole function produces Nothing.

    safeAddHeads :: Num a => [a] -> [a] -> Maybe a
    safeAddHeads l1 l2 = do
        v1  safeAddHeads [1,2,3] [2,4,6]
    Just 3
    > safeAddHeads [1,2,3] []
    Nothing
That do notation get translated to the following uses of bind and return:

    safeAddHeads l1 l2 =
        safeHead l1 >>= \v1 ->
        safeHead l2 >>= \v2 ->
        return v1 + v2
Bind (>>=) implements this behavior, of obtaining a value and feeding it into the next combination, or returning Nothing:

    (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
    Just a >>= f = f a
    Nothing >>= _ = Nothing
Return takes a plain value and makes it a monadic value, so it's just the "Just" constructor:

    return :: a -> Maybe a
    return = Just
(You could also write that as "return a = Just a", but you'll often see Haskell functions written in "point-free style", which means omitting explicit parameter names when not needed.)

So, that's bind and return for Maybe.

For a more unusual example, lists are a monad. The list monad acts like a cross-product: for every value in the list, do the remaining operations. For instance:

    pairs :: [a] -> [b] -> [(a,b)]
    pairs l1 l2 = do
        v1  pairs [1,2,3] [4,5]
    [(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)]
If that looks a lot like a list comprehension, that's not a coincidence.

Return, again, is easy:

    return :: a -> [a]
    return a = [a]
Bind implements the behavior of running the right-hand side for each item on the left and combining the results into a list:

    (>>=) :: [a] -> (a -> [b]) -> [b]
    (x:xs) >>= f = f x ++ (xs >>= f)
    [] >>= _ = []
Note that the list bind operation calls itself recursively here: first call f on the first item, then call it on the rest. This could also be written with a fold or map; for instance:

    xs >>= f = concatMap f xs
concatMap just runs concat on the result of map. The map applies f (of type a -> [b]) to each item in the list (type [a]); since f returns a list, that results in a list of lists ([[b]]), which concat combines into a single list ([b]).

You might want to take a look at the Writer, Reader, and State monads for further examples. Note in particular that those three maintain some internal state information inside the monad, and in addition to the bind and return operations, they offer operations that work with that internal state.

Having given those examples, here's a sketch of how IO could work. IO operations act as though they change some state (the outside world), and if you chain them together, the second operation acts on the state resulting from the first (for instance, if you call getLine twice, the first modifies the state of stdin by reading a line from it, and the second starts where the first left off by reading another line). That state isn't actually representable in Haskell, but let's imagine that it is.

So, the IO type (to focus on the terminology suggested by this article) contains a value along with an operation changing the state of the world:

    data IO a = IOPrivate (StateOfTheWorld -> StateOfTheWorld) a
Notice that a value of IO type contains an operation changing the state of the world, rather than an actual state of the world. It helps to think of an IO type as representing an operation to be run, and main is the top-level operation that actually gets run when you invoke the program. Continuing that analogy, something outside your program, provided by the Haskell runtime, does this:

    run :: IO () -> StateOfTheWorld
    run (IOPrivate f ()) = f initialStateOfTheWorld
(Compare this to functions like runState, for instance.)

Return takes a non-IO value and makes it into an IO value that doesn't actually change the state of the world:

    return :: a -> IO a
    return a = IOPrivate id a
Bind sequences the changes to the state of the world:

    (>>=) :: IO a -> (a -> IO b) -> IO b
    (IOPrivate sf1 v1) >>= f =
        let IOPrivate sf2 v2 = f v1
        in IOPrivate (sf2 . sf1) v2
("sf" here stands for "state function".)

Re: IO Monad Considered Harmful

#39

This is a fair reflection on one of the biggest barriers to newbs learning Haskell (which I've tried to do a couple of times and failed, in part because the syntax is weird and poorly explained, in part because the tutorials suck, and mostly because I'm not very smart, or so I'm told.) The Haskell newb gets told two things very early on: 1) Haskell is pure pure pure! 2) Haskell can do IO via monads! These statements…

To go ahead and be that Haskell purist... in normal Haskell code there are no functions which execute side effects in the process of computing their results. Even a function like

    putStrLn :: String -> IO ()
merely produces an IO value. It is only when such a value is sequenced into something presented to the RTS as `main` that the effects are realized.

This seems like pedantry, but it allows you to construct and compose IO values in many ways (besides just sequentially evaluating them). This ends up giving you "macro like" powers.

Re: IO Monad Considered Harmful

#40
post #22

Earlier quoted context omitted.

IO t is a black box too. If it's a data structure, it's a crappy one: you can't pick it apart like a list, inspect it, or do anything except sequence it. I didn't follow what you meant about "runtime vs evaluation." To me they seem hopelessly intertwined: if you evaluate head [], you get a runtime error.

It's not a black box in the sense you can move it around, evaluate later, not evaluate at all, etc. There is fundamentally nothing you can inspect about a general IO operation: it's a handle to a computation dealing with the external world.

I'm pretty sure you can move black boxes around in the real world too :)

There are definitely sensible things to do with "general IO operations." For example, consider `withArgs`, which replaces argv for the duration of an action. A natural approach is to pick through the received action, and replace all calls to `getArgs` with `return tempArgs`. But `withArgs` doesn't work this way because Haskell can't pick apart IO actions. Instead it uses a nasty FFI to modify a global. (It doesn't even look thread-safe: I'll bet withArgs "leaks" into other threads.)

It's also overbroad: IO encapsulates "real world" computations like deleting files, but also basic operations like getting argv! I'd argue it's a failure of Haskell that the type system does not distinguish between a function that erases your hard drive and a function that gets your command line arguments.

Post reply on HN