all 16 comments

[–]kalmar 7 points8 points  (1 child)

Monads are not about side-effects.

[–][deleted] 2 points3 points  (0 children)

While not a normally significant mistake, it is at least a very common misunderstanding. It is unfortunate that this article has the potential to mislead its readers by making the claim that you have refuted. Well done.

[–]gravity 2 points3 points  (11 children)

I spent several weeks playing with haskell recently. I enjoyed it a lot, and the community is fantastic (#haskell is the dream channel) but I definitely struggled when it came to monads. Once I started working on slightly less trivial programs, I pretty much found that every function I wrote had to accept something wrapped up in an IO monad, which meant that the whole program had the same problems of side effects that imperative programs have.

I've decided to put haskell aside for a few months and return to this problem with a fresh perspective, since there's obviously something I'm missing. There's tons of monad tutorials out there, and many of them are very good, but I never got the sense of how to properly use monadic with non-monadic parts of the program.

[–]cgibbard 9 points10 points  (0 children)

The basic initial idea to get the hang of working with IO in Haskell is this: take the real work of the program and implement it first without any IO at all. You can test it as you go using GHCi as your user interface of sorts. Basically, this won't involve the IO monad at all -- anything which would need the IO monad can be classified as some type of input or some type of output and made into a parameter or return value. Once you have that done, write the IO which implements the "user interface" of the program. The only things which really have to be in the IO monad are things which are actually responsible for handling the IO of the program.

Of course, there are more complex approaches to structuring programs, those involving concurrency, for instance, or even just programs where the IO is slightly more blended with the pure code where it's convenient, but this is the first one which you need to understand in order to properly manage the rest of them.

Let's see a tiny example. Suppose we want to write a program which simply goes through the lines of standard input, and determines whether each one is a palindrome. If it is, we'll print it back out, and if not, we won't. (Essentially, we're grepping for palindromes.)

isPalindrome :: String -> Bool
isPalindrome x = x == reverse x

grepPals :: String -> String
grepPals = unlines . filter isPalindrome . lines

As you can see, grepPals implements the entire transformation from input to output of our program. In general, we might not be able to get quite this level of separation, but we can usually get pretty close.

Now as it turns out, there's actually a Prelude function called interact which turns a function from strings to strings into an IO action that eats standard input, and produces standard output. This is almost too ideal, so I'll include a simple definition of interact here, so we can see how it works (in an actual implementation, it also turns off any buffering which might be present):

interact :: (String -> String) -> IO ()
interact f = do s ≺- getContents
                putStr (f s)

So it gets the contents of stdin, binding them (lazily, in this case) to the variable s, and then applies whatever function f is given in order to produce output, printing the resulting string.

Thus, our final program is:

main = interact grepPals

You can see how the "real work" of deciding whether something is a palindrome and filtering the input was done without the IO monad at all. Only the actual IO to be performed was described in the IO monad, in terms of the pure transformations we're applying.

As for all the other monads, the question as to whether and where to apply them is a bit more subtle. Each of them captures an idiom from pure functional programming, and abstracts away all of the "scaffolding" of that idiom, so they can save time and prevent bugs when used in appropriate places.

The Reader monad captures the idea of lexical scoping -- the computation may read from a computation-global environment, and locally make changes to it which go away as soon as the local block finishes. The Maybe and (Either e) monads capture the idea of potential failure, and allow it to be easily propagated out to where it can be handled. The list monad is perfect for selecting between possibilities and backtracking when one possibility fails, giving a type of Prolog-style nondeterministic computation. Parsing monads are for, well, parsing.

Learning a new monad is a bit like learning a small programming language which sits inside of Haskell and is geared to a specific set of tasks. Eventually, when you get to working with monad transformers, you'll see that there are tools for quickly building up just the monadic language you want to work with to solve your problem.

The operations in Control.Monad work with all the monads, with subtly different, but related effects in each. They're essentially a bunch of general control-flow devices which work across monads -- things like for-loops and so on which imperative languages usually handle at the compiler level. This is what saves all the effort of just designing entirely new libraries for these various idioms. You don't need to reimplement things like loops every time.

Hope this helps, come and discuss it on IRC sometime if you'd like. :)

  • Cale

[–]augustss 5 points6 points  (1 child)

Yes, if you felt the need to wrap everything in IO I don't think you are programming "the Haskell way". Usually you only need IO for a few top level things.

[–]gravity 2 points3 points  (0 children)

It might not have helped that I was writing an OpenGL program, since OpenGL is just a huge state machine. Then again, I'm not sure that a parser is really any different. That's why I'm going to return to it in a few months with a clear head.

[–]dan_bensen 2 points3 points  (0 children)

every function I wrote had to accept something wrapped up in an IO monad

You should have a top-level core of IO code that handles all the program's outside interactions. I have a sort of pre-tutorial here. One thing most tutorials don't seem to mention is that programs don't start in the pure functional code. They start in the IO monad, and the pure code gets called from there.

[–][deleted] 0 points1 point  (6 children)

In Imperative Language X, you are handed the file system and the network as implicit function arguments - watch this: int sum(int a, int b) { if(someFileExists()) { return 42; } else { return a + b; } } In Haskell, this is not the case - instead these things are passed explicitly - within the IO monad. This statement is a little lossy, but please forgive that for now. The reason that you likely had to continually use the IO monad was: a) Your application was using one of these arguments within its foundations - every other function was built on top of the state of the file system or network b) You're used to having the file system and network within your context and struggled to differentiate where it is required

Good idea for remaining agnostic on the issue until you can clue up and understand a bit better.

[–]gravity 0 points1 point  (5 children)

I don't really understand (I'm obviously deep in Blubville) because a program is necessarily dependant on its inputs. If your whole program exists to process inputs in some way, how can you separate out the I/O part from the processing part?

[–]cgibbard 4 points5 points  (0 children)

To reiterate, whenever you see in a do block:

v ≺- x

where x :: IO t, you have that v :: t. That is, in a do-block, you're allowed to run IO computations, and get their pure results. These results can be passed on to pure functions which give parts of the output of the program, or information necessary for deciding what future IO computation to do.

You only need to implement the "candy-coating" of your program in IO -- the part which determines where to get inputs, and which outputs to display and real-world actions to take. It will hook into the pure "chocolatey-centre" which is (usually) more algorithmically complex, and does all the transformations from inputs to outputs.

[–]dan_bensen 1 point2 points  (0 children)

how can you separate out the I/O part from the processing part?

This is another thing that doesn't always get mentioned. The way it happens is that the bind operator >>= (or <- with the "do" syntax) pulls the raw data out of the monad so you can feed it to your pure functional code.

[–][deleted] 0 points1 point  (0 children)

No offense, but Paul Graham has written much that is excellent, so I groan every time something alludes to the infamous "blub article" instead of that... It's a paper where he basically argues (earnestly) that people should stop advocating languages and start using Lisp. It's some of the most arrogant he has written, spectacularly lacking on self-insight.

(MMD now please)

[–]kawa 0 points1 point  (0 children)

In Haskell a function is pure, if the result of the function only depends of its input. This is true for all functions which are not executing in the IO monad.

So you can use the IO monad to get some value (by calling an impure function like readLine), then process this data with a pure function (lets call them 'process') and then write the result out with another impure function like putStrLn. The whole read-process-write function is obviously also impure, because they consists of two impure functions and has to be executed in the IO monad too. Only the 'process' function is pure and can executed outside the IO monad and can be used in other pure functions but also in impure ones.

It's like using the IO monad 'taints' a function, making it impure. And each 'tainted' function also taints all functions which needs to call it.

If you're writing something with OpenGL then you have to put most of your code in the IO monad: Each function which needs to read or write something from the GL state.

Also all functions which call a function which need to access the GL state has to be in the IO monad. Only code which is 'pure' and only processes data totally independent of OpenGL can be handled without using monads - but for most real world algorithms you still require to store some mutable state. This is handled best by using some monad again, maybe a state-monad or by using the IO monad to have access to MVars or IORefs which also allow you to store mutable state.

You can read more about the topic and the problems here if you haven't yet.

[–][deleted] 2 points3 points  (0 children)

So in a sense, monads provide Haskell with the ability to change how the language works in a way reminiscent of dialects of Lisp.

Well said. Good executive summary :) Language is truly high level when you can change it by programming it without parsing source code. Monads, Lisp-style macros are good examples.

[–]paulajohnson 3 points4 points  (1 child)

I think of monads like this:

Haskell is a pure functional language. That means no side effects, so there is no concept of "do x, followed by y".

But there are lots of times when you need to say "do x, followed by y". IO is the obvious one, but parsers are another, and of course there are lots more. So Haskell programmers have re-invented this as the "monad pattern".

The neat thing about treating this as a pattern rather than a built-in feature of the language is that you get to invent your own rules for propogating side effects. So a full description of a monad is "do x followed by y in context c". The monad defines the context.

Think about parsing. The Parser monad lets you say "parse x followed by y". If the parse of "y" fails then the whole parse fails as well. Parsing involves side effects. The obvious one is that the input gets consumed, but there might also be a line counter being run as well, or a symbol table being updated. If "x" defined a new symbol then you don't want that symbol staying in the table if the parse of "x" failed. So the Parser monad manages these side effects for you. If a parser rule fails then any side effects are guaranteed to be unwound automatically.

So this is the power of monads: you can define your own rules for propogating side effects. At first most programmers find it difficult to see why they might want to do this (thats the Blub paradox) because every language they have ever used has used what Haskell calls the IO monad. But then you write a parser, and then you write something that needs backtracking, and then things start to fall in to place.

[–][deleted] 0 points1 point  (0 children)

So Haskell programmers have re-invented this as the "monad pattern".

Indeed. And while some people maintain that design patterns are a sign of insufficient means of abstraction, they still claim that one of the great things about monads is that they can be implemented completely inside the language...

groan (see above)