Live data from Hacker News

IO Monad Considered Harmful

blog.jle.im

61–70 of 75 posts

Re: IO Monad Considered Harmful

#61
post #25

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…

Yeah, no way around learning do-notation even for the simplest of programs. And do-notation is kind of a DSL with a totally different feel than the rest of the language. So getting started writing even simple toy programs in Haskell requires you to learn two languages. No denying that Haskell is a language with a very steep learning curve. (Or is it a very shallow learning curve? Never understood that metaphor. But y…

In operations management, a "learning curve" is an efficiency curve plotted over time. I have most often seen it as a time-per-task-completed curve. The way it works is this:

The time it takes to produce widget 2 ^ t is BASE_TIME * LEARNING_RATE ^ t.

Example with 120 minute starting time and 90% learning rate:

Widget 1 (2 ^ 0) takes 120 (120 * 0.9 ^ 0) minutes to produce.

Widget 2 (2 ^ 1) takes 108 (120 * 0.9 ^ 1) minutes to produce.

Widget 4 (2 ^ 2) takes 97.2 (120 * 0.9 ^ 2) minutes to produce.

Widget 8 (2 ^ 3) takes 87.48 (120 * 0.9 ^ 3) minutes to produce.

Example with 120 minute starting time and 70% learning rate:

Widget 1 (2 ^ 0) takes 120 (120 * 0.7 ^ 0) minutes to produce.

Widget 2 (2 ^ 1) takes 84 (120 * 0.7 ^ 1) minutes to produce.

Widget 4 (2 ^ 2) takes 58.8 (120 * 0.7 ^ 2) minutes to produce.

Widget 8 (2 ^ 3) takes 41.16 (120 * 0.7 ^ 3) minutes to produce.

If you plotted a curve of minutes per widget over time, you would note that the graph for the 70% learning rate would have a much steeper slope than that of the 90% learning rate.

Looking at that you might say, "ah, high learning rates are great!" All else being equal, you would be correct. However, real world tasks with high learning rates often imply high base times and a proportionately large amount of time until proficiency or mastery is reached. That's why "steep learning curves" are "bad things", because it is going to take "a lot of effort" just to "get good" at the skill.

To bring the point home, here are some contrived examples of "90%" and "70%" skills:

Version control:

90% - copying and pasting files

70% - git

Word processing:

90% - Microsoft Word

70% - vim and latex

Databases:

90% - Excel documents in a shared folder

70% - PostgreSQL

Web design:

90% - your web host's website builder

70% - Ruby on Rails

70% - hand coding in notepad

This is just an introduction to the concept as I learned it. Two skills might have the same learning rate, but one might be much more productive than the other. Still this should help you understand the origin of "steep learning curve" and why that's usually considered a "bad thing."

Re: IO Monad Considered Harmful

#62

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

A pointfree solution - if you want one - would be more in the form of:

    getLine >>= putStrLn . ("Hello, " ++)
It's really mostly about getting used reading pointfree style, but there's certainly an overuse of pointfree style in some Haskell code, then it gets pointless.

Re: IO Monad Considered Harmful

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

Every explanation of every Haskell feature I encounter reads the same way as this. You define "return" as " return :: a -> m a " and say this means it takes "an a" (by which I assume you mean an object of type a?) and returns a " Foo " (an object of type Foo ?). The problem I have is: what on earth does this have to do with 'm'? Is 'm' equivalent to 'Foo'? If so, why don't you mention that? It isn't obvious to a newb…

The 'm' is a type variable. This is a consequence of a difference in how Java interfaces and Haskell typeclasses are defined. (I'm continuing wyager's analogy between Java interfaces and Haskell typeclasses here.)

Here's a sample Java interface:

  interface Pushable {
    void push(bool shouldShove);
  }
Here's a similar Haskell typeclass:

  class Pushable p where
    push :: p -> Bool -> ()
In type signatures, an identifier starting with a lowercase letter is a type variable. So 'p' is a type variable here, like the 'm' type variable used in the Monad typeclass. It refers to the type which, in Java terms, implements Pushable. (In Haskell you'd say the type "is an instance of Pushable".)

So why this difference? Java, being a C++-style OO language, privileges the implicit first argument. When class Button implements Pushable, you can think of its push method as having a real signature of "void push(Button this, bool shouldShove)". This particular pattern is special in Java, so you don't have to write the "Button this" parameter explicitly. But it's not a special pattern in Haskell, so you do need to explicitly write the 'p' in "push :: p -> Bool -> ()".

To drive the point home, maybe one more comparison is useful. In Java, when an interface method needs to take a parameter that has the type of the implementing class, you need to do something that looks a bit more like Haskell. Here's an example:

  interface Comparable {
    Ordering compare(T other);
  }
If you're not familiar with this idiom (which is the same as the CRTP in C++), here's how you'd implement Comparable:

  class Foo : Comparable {
    Ordering compare(Foo other) { ... }
  }
Now compare the type parameter 'T' in our Java Comparable interface and the type parameter 't' in this Haskell Comparable typeclass:

  class Comparable t where
    compare :: t -> t -> Ordering
They serve the same role - they refer to any type that may implement that interface (in Java) or be an instance of that typeclass (in Haskell).

Hopefully this clarifies things a bit.

Re: IO Monad Considered Harmful

#65

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…

My take on how to explain basic IO without having to explain monads:

(1) Any function with interacts with the world is called an IO function and has the result type "IO something". e.g. "IO String" for getLine (because it returns a String). Functions and expressions which does not interact with the world are called 'pure'. IO is a type constructor, but for now it makes sense to think of it as a "tag" on result values which guarantees we cannot mix IO-functions with pure functions. If we could call IO functions inside pure functions, the whole idea of pure would dissolve. The IO "tag" allows the compiler to enforce that this can't happen.

(2) A function which calls IO functions uses 'do'-notation:

    somefun = do
	putStrLn "Hello"
	putStrLn "World"
Each line is a single IO function call (with arguments which are pure expressions). Each line in the do-block should be indented to the same level. (Only in the case where the function consist of just a single IO-call can the 'do' be left out, which is why you can write 'main = putStrLn "hello World"'). The do-notation guarantees that the operations are executed sequentially. The return value of the function is the return value of the last operation in the do-block.

If you want to use the return value of an IO function, you use a left-arrow (

    somefun = do
        putStrLn "What is your name?"
	name 
Note that you cannot simplify the two lines to:

    X	putStrLn ("Hello, " ++ getLine)
The above breaks the rule that each IO function call have to be on a separate line in the do-block. IO-function calls can not be nested in expressions, they have to be sequential.

(3) Calling pure functions. You cannot call IO function from pure code, but you can call pure functions from IO-code. This is done via 'let':

    somefun = do
        putStrLn "What is your name?"
	name 
Note how the let syntax is distinctly different from the left-arrow syntax. The let syntax just assigns the result of a pure expression to a variable. The arrow syntax "extracts" a pure value from an IO value. The result of getLine is 'IO string' but the arrow "untags" it so the type of name is just "String", and it can be used as part of a pure expression.

(4) Returning pure values. The return type of a IO function has to be IO something. In the above example the return type is 'IO ()' since this is the result type of the last operation, putStrLn. But what if we want to return say the reversed name? The reversed name is a pure value, so this is will not compile:

    somefun = do
        putStrLn "What is your name?"
	name 
We have to turn the String into an "IO String", because the result type of an IO function has to be "IO something". This is done with the "return" operation, which turns a pure value into the corresponding IO value. The name "return" is unfortunate since it looks equivalent to "return" in imperative languages. But in Haskell "return" doesn't actually exit the function. The function exits after the last operation regardless of the use of "return". The only thing "return" does is to "tag" a pure value so we can return it from an IO function:

    somefun = do
        putStrLn "What is your name?"
	name 

Re: IO Monad Considered Harmful

#66
post #30

Earlier quoted context omitted.

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)

If this is meant to be under main, you need a "let": main = do let greet name = putStrLn ("Hello, " ++ name) getLine >>= greet For whatever reason, I didn't learn about the magic let syntax in do-notation until relatively late. Anyways this is very elegant, but not something I could have generated early on, especially because it mixes two different sequencing syntaxes (do-notation and >>=).

The 'do' is superfluous.

It would be much better as

    main = let greet name = putStrLn ("Hello, " ++ name)
                in getLine >>= greet

Re: IO Monad Considered Harmful

#67

Ok, so we can't use the word monad because that's bad apparently. How do we answer "How does Haskell, a FP language where composition is a very important concept, compose IO actions?". Because that's almost always done using monadic actions. Aside, I agree on the List monad thing. I never ever use the list monad, why would I. But if I'm managing state (i.e. IO, Reader/Writer, Conduit etc), then why wouldn't I use the…

> Aside, I agree on the List monad thing. I never ever use the list monad, why would I.

I use monad library functions on lists happily, and use the do-notation, particularly when I'm thinking in terms of non-deterministic computation and search.

Re: IO Monad Considered Harmful

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

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

Indeed, but there were people in this thread who were asking about them, and I will always prioritize people who are asking to learn something over people who are asking not to teach something.

Re: IO Monad Considered Harmful

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

Every explanation of every Haskell feature I encounter reads the same way as this. You define "return" as " return :: a -> m a " and say this means it takes "an a" (by which I assume you mean an object of type a?) and returns a " Foo " (an object of type Foo ?). The problem I have is: what on earth does this have to do with 'm'? Is 'm' equivalent to 'Foo'? If so, why don't you mention that? It isn't obvious to a newb…

You're absolutely right; that is confusing. Swift's explanation in response to your comment is good, so hopefully that helps.

There are a few reasons I wrote "m a".

When you use a lowercase letter in a type definition (like "m" or "a"), that's a type variable. The type isn't anything in particular. In a function like

    id :: a -> a
that "a" can become anything when you use the function later. So you can use "id" on integers, or floats, or strings, or whatever. And with functions like

    add3numbers :: Num a => a -> a -> a -> a
that "a" can become anything that's an instance of the "Num" typeclass (in Java: anything that implements the "Num" interface). "Num" is a restriction on the value of "a", but "a" is still variable.

If I were to write a function that had an uppercase word in the type, like

    doFooThing :: Foo -> Foo
an uppercase type indicates that this isn't a type variable, but rather a specific type. In this case, the function only takes and returns the type called "Foo". So that's why I used a lowercase word in my definition of ">>=", as it works on any Monad, not just Foo. Here's how you actually write out the definition of the Monad typeclass:

    class  Monad m  where
        (>>=)       :: m a -> (a -> m b) -> m b
        return      :: a -> m a

In the definition of >>=, we don't define it in terms of Foo or any other specific Monad; we define it in terms of a type variable (which is idiomatically "m", for "Monad"). "m" can then be filled in with any Monad (like Foo, or Maybe, or IO) when the time comes to actually use ">>=".

You could also define Monad with

    class  Monad qwerty  where
        (>>=)       :: qwerty ping -> (ping -> qwerty pong) -> qwerty pong
        return      :: dazzle -> qwerty dazzle
As you can see, this is not as nice as e.g. "a -> m a"

So it would have been more correct for me to have said "here's the type signature for >>=, and when you use >>= with something called Foo that implements the Monad interface, it >='s type in terms of Foo>."

Re: IO Monad Considered Harmful

#70
post #41
post #33

"Avoid success at all costs" comes to mind.

As per usual, it was meant to be read as "Avoid: (Success at all costs)" not "(avoid success) (at all costs)".

My understanding is more that it was meant to be read both ways, but with the former understood to be serious and the latter a joke.
Post reply on HN