Live data from Hacker News

IO Monad Considered Harmful

blog.jle.im

51–60 of 75 posts

Re: IO Monad Considered Harmful

#51
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 who is lost in the welter of syntax.

Does 'm a' mean the same thing as Foo? If so, why not use 'Foo' in your Haskell rather than 'm', which is simply introduced without any description, mention or explanation? Is there some significance to the naming? This is the kind of question that a newb specifically cannot answer, and Haskell tutorials are notoriously lax about. They have a strong tendency to assume you know the syntax of the language, which they use to explain the syntax of the language.

You've given a nice, detailed explanation, but have failed to describe what 'm' is, and that means your average newb (me, or possibly even someone much, much smarter than me, as I'm told such people exist in profusion) is left making guesses. I'm pretty sure I know what the correct guesses are, but I really want to know: why did you use a completely different, unrelated, and opaque name for 'Foo' in your Haskell example, and then talk exclusively about 'Foo' in your explanation?

This isn't really a knock at you personally: as I said, every Haskell tutorial I've encountered has similar issues. I'm just deeply curious as to why this is the case (and painfully aware that my own explanations regarding other languages are likely as opaque to newbs as these Haskell explanations are to me.)

Re: IO Monad Considered Harmful

#52

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…

He actually does go into that in the "Some side notes" part. He recommends calling it a monad only when you actually use the monadic interface:

“I have to directly use the result of one IO action in order to decide which IO action should happen next”: Yes, this is a use case for IO’s monadic interface."

Then yes - you need the do-notation.

For simple sequencing he recommends to use it's Applicative instance.

Your overall point is correct though - choosing to avoid the do notation in Haskell will likely limit your ability to read and understand a lot of code just because monads are so commonplace in Haskell.

Re: IO Monad Considered Harmful

#53
post #27

I've also experienced similar failures. I've tried to learn Haskell several times, and every time I meet the Monad section in the tutorial I lost my concentration. One of my friends who is proficient in Haskell told me to just start coding without trying to understand the advanced concepts, but I wasn't inclined to do that as Monads were hanging over my head, and I really wanted to understand what they are before wri…

There are lots of things in Haskell that I miss when I use rust, so I'd still encourage you to learn it. For example, concurrency in rust is OK, certainly nicer than other imperative languages, but Haskell has easily an order of magnitude more awesome to offer.

I recently wrote a library for testing concurrent code. It's basically fuzz testing of scheduler behaviour to root out race conditions. In most languages, you'd need a special compiler or runtime to do this, in Haskell it's a library.

Re: IO Monad Considered Harmful

#54
post #48

Earlier quoted context omitted.

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…

There's one thing that should be mentioned here: you can easily compose these things. You can do: echo = forever $ getLine >>= putStrLn This also works with do notation, if you'd want to make the greet example looping. greetLoop = forever $ do name This is a very powerful concept. In mainstream imperative programming languages, you can't assign "statements" to variables or return them from functions or pass them as p…

Maybe I'm misinterpreting you, but isn't that effectively what a closure is in an imperative language?

    def forever
      loop do yield end
    end

    forever do
      name = gets
      puts "Hello, name"
    end
or even

    template 
    void forever(const F& fn) { for (;;) fn(); }

    int main()
    {
      forever([]
      {
        std::string str;
        getline(std::cin, str);
        std::cout 
looks a lot like the above code to me.

Re: IO Monad Considered Harmful

#55
post #22

Earlier quoted context omitted.

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 I…

You either enforce 100% purity, or your language is not pure. Haskell chose to walk the former path. In that case reading global state needs to happen in the IO monad even if it is reading a couple of command line arguments. Though I completely agree it is nasty to change them in runtime, it is also assumed you know what you're doing when you tinker with them (cross-thread implications included.)

To a Haskell program, command line arguments are exactly the same outer world as your super important files.

Re: IO Monad Considered Harmful

#56
post #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 comp…

Thanks for this answer. I think it points clearly to the source of confusion.

To be pedantic back at you, I'm pretty sure your explanation translates to: "So long as you never run a program, Haskell is a pure language."

Pedantically speaking, you can't have it both ways. Either the "Haskell" of the claim "Haskell is pure" includes the RTS or it does not. If it does, it is not pure but can run programs. If it does not, it is pure but cannot run programs.

A programming language that you can't run programs in is not very useful or interesting, so I'd really think it was a better move to define "Haskell" in such a way that programs can be run, and accept that under that definition Haskell is impure, although it retains a kind of effectively pure formalism that allows provability and such, which is awesome.

Either way, it is really important to emphasize in any description of the language that "At runtime Haskell is not pure". Otherwise newbs like me will simply be confused by the contradiction between the claim "Haskell [considered as an unexecutable language] is pure" and "Haskell [considered as a set of instructions to the RTS] is capable of doing IO". The two completely different meanings of "Haskell" must be made clear if profound confusion is to be avoided.

Re: IO Monad Considered Harmful

#57
post #46
post #32

Earlier quoted context omitted.

> getLine >>= (return . (++) "Hello, ") >>= putStrLn And my coworkers yesterday were complaining that ClojureScript looked like Brainfuck... ( http://en.wikipedia.org/wiki/Brainfuck )

That's a very contrived example you obviously didn't spend any time trying to understand. No-one in their right mind would write code like that, except to demonstrate something. And if you did write code like that, Haskell's hlint tool would make suggestions to turn it neat and readable using fmap.

Why wouldn't I want to write that? Clearly you need a little practice before instantly recognizing a partially applied function where the next parameter will be appended to your other string, but that one-liner has actually a very nice computing flow.

Re: IO Monad Considered Harmful

#58
post #54
post #48

Earlier quoted context omitted.

There's one thing that should be mentioned here: you can easily compose these things. You can do: echo = forever $ getLine >>= putStrLn This also works with do notation, if you'd want to make the greet example looping. greetLoop = forever $ do name This is a very powerful concept. In mainstream imperative programming languages, you can't assign "statements" to variables or return them from functions or pass them as p…

Maybe I'm misinterpreting you, but isn't that effectively what a closure is in an imperative language? def forever loop do yield end end forever do name = gets puts "Hello, name" end or even template void forever(const F& fn) { for (;;) fn(); } int main() { forever([] { std::string str; getline(std::cin, str); std::cout looks a lot like the above code to me.

Well, in this simple example it is trivial to do the same by wrapping statements in a closure, but it's a bit more nuanced than that. But you still can't assign a for-loop as a value to a variable, for example (to exaggerate a little).

This becomes more apparent when using monads for non-IO applications like parsing. It's difficult (but not impossible, of course) to implement things like backtracking or non-determinism by writing lots of closures in an imperative programming language, they don't compose as nicely. There are parser combinator libraries (similar to Parsec) done in other languages of course, but some of them even implement monads in the language they work in. I recall seeing "class Monad" written in Python, I think it was in the pyparsec library.

Re: IO Monad Considered Harmful

#59
post #43

Earlier quoted context omitted.

The issue isn't as great as you portray it to be. You're facing beginner issues and learning a foreign topic, expect to spend some time on it and put in the mental effort and you'll master it in no time. After spending an hour to write "Guess the number" game with and without do notation, you should know enough to be able to write practical programs in Haskell. Yes, an hour for guess the number is a lot, but it's the…

I have mastered it, but I have not drunk so much Kool-Aid that I have forgotten what it took! It sure did not take "no time," and much longer than an hour - longer than any other programming language I have attempted. The biggest stumbling blocks were syntactical. For example, there are three ways to declare a variable (let-in, let, and learning was not so much new concepts but instead things like how a "not in scope…

Perhaps you interpreted me a bit too literally. Of course it takes time for internalizing new and foreign concepts. The lecture, lesson or exercise might only take an hour but it may take days or weeks to grok the concepts. And this might mean a lot of practice.

I certainly remember having a hard time understanding this years ago, and my problem was probably the same as yours - the lack of nice books and tutorials (but things are a bit better now) as well as it all being new and foreign. I don't regret putting in the effort, though.

I also do not like the inconsistency in let-in vs. let (do notation), but I guess there's a good reason for that. But I don't think these issues can be "fixed" without turning Haskell into something completely different (e.g. something more like ML). Or do you have a good suggestion how to simplify this without making it worse?

Re: IO Monad Considered Harmful

#60
post #57
post #46

Earlier quoted context omitted.

That's a very contrived example you obviously didn't spend any time trying to understand. No-one in their right mind would write code like that, except to demonstrate something. And if you did write code like that, Haskell's hlint tool would make suggestions to turn it neat and readable using fmap.

Why wouldn't I want to write that? Clearly you need a little practice before instantly recognizing a partially applied function where the next parameter will be appended to your other string, but that one-liner has actually a very nice computing flow.

That's a bit hard to read, isn't it? (return . f) is a common idiom (or antipattern?) which is better solved by using fmap. hlint will tell you this. For example:

    fmap ("Hello, " ++) getLine >>= putStrLn
This isn't necessarily better either, I'd write this using do-notation.

I understood every bit of that code, I just think it's ugly and hard to read and there are better ways to get the same done.

Post reply on HN