5 ms·
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 condit
by tmhedberg 14y ago
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:
fooResult = foo(x);
if (fooResult == null) {
return null;
} else {
barResult = bar(fooResult);
if (barResult == null) {
return null;
} else {
bazResult = baz(barResult);
if (bazResult == null) {
return null;
} else {
quuxResult = quux(bazResult);
if (quuxResult == null) {
return null;
} else {
return quuxResult;
}
}
}
}
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.
- jQueryIsAwesome 14y agoI do a lot of Javascript and I would do it this way: [foo,bar,baz,quux].reduce(function(p,c,i){ return p == null ? null : c(i === 1 ? x : p) }) And you could abstract it in the Array prototype Array.prototype.linearExe = function(x){ return this.reduce(function(p,c,i){ return p == null ? null : c(i === 1 ? x : p) }) } To use it like this [foo,bar,baz,quux].linearExe(x)
- tmhedberg 14y agoTrue, 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!
- Peaker 14y ago[foo, bar, baz, quux] might be problematic if the types of these functions are not homogenous. foo >=> bar >=> baz >=> quux is ok and >=> is not a much-worse separator than , here...
- tikhonj 14y agoIf 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.