Live data from Hacker News

IO Monad Considered Harmful

blog.jle.im

11–20 of 75 posts

Re: IO Monad Considered Harmful

#11
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:

Here's how to print a string (putStr, putStrLn). Here's how to print something other than a string (show, print). Here's how to print two somethings in a row (>>). Here's how to read something and print that (>>=). Here's how to print something in a function other than main (:: IO ()). Here's how to read something in a function other than main (:: IO a, >>= again, return). Oh by the way, it turns out that pattern (return, >>=) is so common that Haskell gives it a special syntax (do notation). For instance, it works with the previously introduced Maybe. And with List. Remember typeclasses? You can write functions that work with IO, Maybe, and List (Monad m => ...).

Even when actually talking about monads, the explanation should come first, and the term should come later. "It turns out these operations are a common enough pattern that Haskell gives them special syntax..." comes across much more sanely than "now we're going to introduce something [complicated] called a monad".

Re: IO Monad Considered Harmful

#12
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.

Re: IO Monad Considered Harmful

#13
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…

That might be a decent explanation of monads, but the article's point is explicitly that we shouldn't worry beginners with monads at all.

Re: IO Monad Considered Harmful

#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; it's not necessary afaik to draw this distinction in other languages, which I think is where the confusion comes from.

In Haskell evaluation is pure, the runtime is not, but evaluation is what allows you to reason about your program.

Re: IO Monad Considered Harmful

#15
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?

Think of a value with type (IO String) as "a computation that interacts with the outside world and then returns a value of type String", and similarly (IO ()) is "a computation that interacts with the outside world and then does not return a value. putStrLn has type IO (). getLine has type IO String.

Now, imagine that you want to build up a whole program that interacts with the outside world. One easy way to do this is to sequence together a number of these primitive operations. The monadic bind operator is Haskell's way of sequencing together computations that support the monad interface (which IO does). A simple console echo program could be written as "main = getLine >>= putStrLn", using the >>= operator to feed the result of getLine into putStrLn. The two computations, sequenced together, form the definition of the whole program.

The monadic bind operator gets awkward if you're trying to combine lots of these statements or if you need to assign their results to variables, and so do-notation was added to the language:

  main = do
    youTyped 
This is because a monad functions just like an overridable semicolon: it's a way for you to define how two computations, specified in sequence, will execute. For the IO type, this is specified by the language runtime to be "execute the first, then execute the second", but it doesn't have to be this. For the Maybe type, for example, it's "Execute the first, but only execute the second if the result of the first is not Nothing", and for the Error type, it's "Execute the first, but propagate the error if it returns one."

A key point the article is making but fails to mention is that these values of the IO type are just ordinary values. The "normal" thing to do is execute them in sequence, but you don't have to do that. You can, for example, map putStrLn over a list and then sequence that with mapM:

  main = mapM putStrLn [1,2,3,4]
Or you could just map putStrLn over a list, and then pass the resulting list of IO values around to some other filter for further processing before sequencing. A somewhat roundabout way of defining FizzBuzz, for example, would be:

  subst (i, a)
    | i `mod` 15 == 0 = putStrLn "FizzBuzz"
    | i `mod` 5 == 0 = putStrLn "Buzz"
    | i `mod` 3 == 0 = putStrLn "Fizz"
    | otherwise = a
  makePrint i = (i, putStrLn $ show i)
  main = sequence_ $ map (subst . makePrint) [1..100]
See what I did there? The numbers 1 through 100 are each first passed through makePrint, which pairs them with an IO action to print their string equivalent. The result of that is passed to subst, which filters out multiples of 3, 5, and 15 to instead become actions that print "Fizz", "Buzz", and "FizzBuzz". The original putStrLn calls are dropped on the floor, or more accurately, never executed because Haskell is lazy. Finally, all of the resulting actions are sequenced together with sequence_, which is a standard library function to execute a bunch of monads and throw away the result. The result of putStrLn is just a value, and can be manipulated like any other value.

This is how many of the cooler monadic libraries (eg. stateful CGI programming with WASH, parser combinators in Parsec, or bytecode JIT generation with LLVM) are implemented.

Re: IO Monad Considered Harmful

#16
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…

Did you even understand that the whole point of the article was about not bringing up Monads?

Jesus.

Re: IO Monad Considered Harmful

#17
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 go -> that way, or how to call a function, is tossed out the window. And you'll spend ten minutes before you realize your indentation is wrong and then ten more fighting with how you declare a variable and then apply a function to it before it dawns on you, this is monads.

You don't have to use do-notation, sure. You can use >>= as an infix operator. It's still totally confusing.

The reality is that monads hit you hard and fast as soon as you want to do any IO. Not the monad laws or functors or whatever the tutorials focus on, but the nitty-gritty syntax. This is where I got stuck and gave up (twice), before I finally got it.

Re: IO Monad Considered Harmful

#18
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.

Re: IO Monad Considered Harmful

#19
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…

I see that you have invoked ire with your comment but I have to say: 1. I have never even attempted to grok Haskell 2. I read the article, noted that the author never actually defined 'monad' even those he raised the question, and asked myself 'yes, but what is a monad?' 3. I was going to Google it, but decided to check these comments for some concise answer so that I might avoid some kind of Haskell rabit hole when I should be coding.

I found your comment to be just what I was looking for and written quite concisely. So, thanks, and haters gonna hate.

Re: IO Monad Considered Harmful

#20
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.

well, maybe gray then; it's not completely opaque. You know it's type, you can apply functions to the value inside. You can pass it around like a variable and if desired, completely throw it away. the point was to show that this is different than some bytecode.

I'm not sure about the head [] example, it might actually be represented by a pure value such as _|_ (bottom) which bubbles up and stops your program when running.

Post reply on HN