This is one of the reasons why I don't use Haskell.
Every concept you mention is made more complicated thinking about it with monads. Every time I use Haskell I feel like too much mental energy is wasted on silly stuff like this.
Yeah, although the article mentions how monads eliminate some boilerplate code, the whole concept of monads is lots of boilerplate code that doesn't exist in other languages. I don't really see the point of writing something like a web server or anything that interfaces a lot with users in Haskell. Seems like its strengths are manipulating data and parallelism, but it would make more sense to use a wrapper around it to handle I/O.
Monads are important in providing well-defined semantics for code. They are a way of taking imperative code and making it functional; the semantics of functional computation are much easier to define than those of imperative code. If you were interested in proving your program correct with a deductive proof (rather than, say, statistically acceptable with tests) , monads would be very important to you.
They weren't designed to provide useful design patterns. That's why articles attempting to demonstrate them as useful design patterns often seem artificial. But I think that as the art of software engineering progresses, the problems they solve will become more important.
Let's say I have several functions I want to compose, feeding the result of one as the argument to the next. But each of these functions has some failure condition, which, if it occurs, causes the function to be unable to return a normal result. In an imperative language, I might represent that failure by returning a null reference, and I'd write something like this:
I think that's a pretty common pattern: call a function and check whether it succeeded. If it did succeed, then pass its result to the next function and repeat. If it failed, then propagate the failure outwards, skipping the rest of the computation.
But that's pretty tedious and repetitive code to write. The signal to noise ratio is incredibly low. Not only that, but if I get lazy or forgetful and don't check for null in any one of those cases, my code could blow up at runtime. It would be nice if we could abstract out the pattern of error checking and propagation of errors so we didn't have to type so much boilerplate around the real meat of the computation, which is just the four function calls. Haskell's Maybe type serves precisely this purpose. The exact same code as above, in Haskell using the Maybe monad, would be:
return x >>= foo >>= bar >>= baz >>= quux
That's it! Error propagation will happen automatically, and if no errors occur along the way, we'll get the same result at the end.
Now, you might think that there must be a lot going on behind the scenes to make that one-liner actually do what we want it to do. But there isn't really. Maybe comes for free in the standard libraries, and even if it didn't, we could trivially define it ourselves like this:
data Maybe a = Nothing | Just a
instance Monad Maybe where return = Just
Nothing >>= _ = Nothing
Just x >>= f = f x
That's the entire definition of the Maybe data type and its complete error propagation behavior. There's nothing more to it than that. And not only will it work in the specific example I gave, but in any situation where I have a chain of functions to call that might fail in some way. After writing that once (or not at all, since it's in the libraries), I can now apply that pattern anywhere I need it, in any program, to concisely express what took around 20 lines of onerous and error-prone repetition without the benefit of the monad.
That's how monads eliminate boilerplate, and one of the reasons why you might benefit from understanding them even if you're not a Haskell user.
Second, in most cases ignoring errors is not what you want. Knowing which function returned null, and why it returned null, is almost as important as the null itself.
Ideally functions would raise an exception instead of returning bad data (aka null) and the psuedo-code snippet above would more cleanly be written:
I concede on the pseudocode formatting point. Upon rereading, that is uglier than necessary.
The trouble with exceptions is that, unless you're referring to Java-style checked exceptions, there's nothing that forces you to handle them. You can still forget that the exception can occur, and then your code will blow up at runtime. And in one sense, exceptions are even worse than the repetitive if-then clauses: if statements at least handle the failure right there at the call site, whereas exceptions let you move your error handling code to some other place in the call stack, non-local to where the actual error might occur. There are advantages as well, for sure, but I see this as potentially very confusing.
Haskell has exceptions but most people prefer to avoid them, favoring the use of MonadError instances (such as Maybe) instead, since they make the potential failure highly explicit and impossible to forget about or ignore. And while I don't think using Maybe is at all the same as ignoring errors, I suppose I see how it could be construed that way. That's why there's Either as well, which allows you to associate some piece of data with the failure. So you could use the Either String monad to pass back an error message on failure, or Either [String] to pass a list of them, or Either (IO ()) to specify an action to be taken in response to the failure, or whatever you want. I guess that ends up being a lot like exceptions, except for the fact that you can't ignore or forget them, so your code is still verifiably safe at runtime, and there is nice syntactic sugar from the Monad class (do notation, >>= chaining, etc.) to make them concise and elegant to use without cluttering your code.
So, Java's HashMap.get() should throw a checked exception whenever the value is not found in the map? That would sure eliminate a lot of boilerplate code :)
Aren't exceptions meant to be used for, well, exceptional cases only, i.e. when no matter what, execution really can't be continued? It makes sense for a lot of functions to return some sort of "Nothing" value instead of raising an exception, but the problem is that in many languages there is no explicit "Nothing" value, so people just abuse null.
You can have Maybe in Java too, but getting out a value or a Nothing out of a Maybe value is just not enforced by the compiler due to the lack of algebraic data types and pattern matching; still, it is an improvement over null hell. On a side note, Maybe is called Option in Scala.
By the way, exceptions are monads, they are just predefined in most languages in a way you can not change. If you want a lengthy Java centric read about that I would strongly recommend checking out [1]. Ant here's another if you're ok with reading Haskell [2].
Also for the record, in Haskell you can use the ErrorMonad in place of the Maybe monad if you want precise error reporting capabilities.
Exceptions can be modeled by a type like "Maybe" that simply also has the error information in it. So instead of a "Nothing", you have "Left err" with the "err" information in it.
The nice thing is that the same example using >>= above would work with Either's too, because it will just use the monad instance of Eithers, and you will get all the error information that way.
Exceptions, when represented this way, become a way that the error is handled by the caller rather than a different way to return. That way, the result of the function is well-described by its type.
Clearly having composable functions is a powerful idea. That's essentially part of the reason shell scripts are so successful. A well behaved shell script is a F(Lines)->Lines. which means you could actually write the above code like this:
x|foo|bar|baz|quux
And any step in the pipeline produced an empty output, that would be automatically carried through. This is one reason why having a.b.c through a NPE in java is not always ideal. In objective-c you can pass any message to nil, so [[a b] c] is defined even if a.b is nil. Imagine what a pain it would be if `cat file | grep a | grep b` blew up because file was empty or a was not found.
True, depending on your language, you can probably get the imperative version to be shorter than my pseudo-C implementation.
But that accumulating function you're passing to reduce? That's essentially just a JavaScript version of the bind (>>=) operator for Maybe. In fact, in Haskell, I'd express what you wrote as:
foldl (>>=) x [foo, bar, baz, quux]
foldl is Haskell's version of reduce, so that's almost exactly the same as your code. You're using a monad to shorten your code, you're just not being obvious about it!
If you take this pattern and generalize it to having arbitrary behavior between each function call, you basically have monads where the [foo,bar,baz,quux] bit sort of acts like do notation in Haskell. The exact details might not be entirely the same, but it is exactly the same idea: you can control how a bunch of functions get composed.
So you'd have code that looks just like yours, but depending on which "monad instance" you parametrize it with, it could have different behavior. So you could uniformly represent code that could return null, code that could have multiple results or even parser code. And, very importantly, you could write other functions that act over all these different behaviors. So you could implement a reduceM function that would support a reducer that could return null, or one that could return many elements, or plenty of other possible behaviors in a uniform way.
Every concept you mention is made more complicated thinking about it with monads. Every time I use Haskell I feel like too much mental energy is wasted on silly stuff like this.