40

I wonder what are the advantages of Maybe monad over exceptions? It looks like Maybe is just explicit (and rather space-consuming) way of try..catch syntax.

update Please note that I'm intentionally not mentioning Haskell.

gnat
  • 21,442
  • 29
  • 112
  • 288
Vladimir
  • 503
  • 1
  • 4
  • 8

7 Answers7

65

Using Maybe (or its cousin Either which works basically the same way but lets you return an arbitrary value in place of Nothing) serves a slightly different purpose than exceptions. In Java terms, it's like having a checked exception rather than a runtime exception. It represents something expected which you have to deal with, rather than an error you did not expect.

So a function like indexOf would return a Maybe value because you expect the possibility that the item is not in the list. This is much like returning null from a function, except in a type-safe way which forces you to deal with the null case. Either works the same way except that you can return information associated with the error case, so it's actually more similar to an exception than Maybe.

So what are the advantages of the Maybe/Either approach? For one, it's a first-class citizen of the language. Let's compare a function using Either to one throwing an exception. For the exception case, your only real recourse is a try...catch statement. For the Either function, you could use existing combinators to make the flow control clearer. Here are a couple of examples:

First, let's say you want to try several functions that could error out in a row until you get one that doesn't. If you don't get any without errors, you want to return a special error message. This is actually a very useful pattern but would be a horrible pain using try...catch. Happily, since Either is just a normal value, you can use existing functions to make the code much clearer:

firstThing <|> secondThing <|> throwError (SomeError "error message")

Another example is having an optional function. Let's say you have several functions to run, including one that tries to optimize a query. If this fails, you want everything else to run anyhow. You could write code something like:

do a <- getA
   b <- getB
   optional (optimize query)
   execute query a b

Both of these cases are clearer and shorter than using try..catch, and, more importantly, more semantic. Using a function like <|> or optional makes your intentions much clearer than using try...catch to always handle exceptions.

Also note that you do not have to litter your code with lines like if a == Nothing then Nothing else ...! The whole point of treating Maybe and Either as a monad is to avoid this. You can encode the propagation semantics into the bind function so you get the null/error checks for free. The only time you have to check explicitly is if you want to return something other than Nothing given a Nothing, and even then it's easy: there are a bunch of standard library functions to make that code nicer.

Finally, another advantage is that a Maybe/Either type is just simpler. There is no need to extend the language with additional keywords or control structures--everything is just a library. Since they're just normal values, it makes the type system simpler--in Java, you have to differentiate between types (e.g. the return type) and effects (e.g. throws statements) where you wouldn't using Maybe. They also behave just like any other user-defined type--there is no need to have special error-handling code baked into the language.

Another win is that Maybe/Either are functors and monads, which means they can take advantage of the existing monad control flow functions (of which there is a fair number) and, in general, play nicely along with other monads.

That said, there are some caveats. For one, neither Maybe nor Either replace unchecked exceptions. You'll want some other way to handle things like dividing by 0 simply because it would be a pain to have every single division return a Maybe value.

Another problem is having multiple types of errors return (this only applies to Either). With exceptions, you can throw any different types of exceptions in the same function. with Either, you only get one type. This can be overcome with sub-typing or an ADT containing all the different types of errors as constructors (this second approach is what is usually used in Haskell).

Still, over all, I prefer the Maybe/Either approach because I find it simpler and more flexible.

Tikhon Jelvis
  • 5,206
  • 1
  • 24
  • 20
  • Mostly agreed, but I don't think the "optional" example makes sense - it seems like you can do that just as well with exceptions: void Optional(Action act) { try { act(); } catch(Exception) {} } – Errorsatz Feb 15 '19 at 20:05
  • "This is actually a very useful pattern but would be a horrible pain using try...catch" is it? Maybe I'm missing something but it looks a pretty straightforward loop with one try catch in it. in try block you return, in catch you continue the loop. Then you also "return a special error message" after the loop. – Alireza Mirian Jun 24 '21 at 19:17
  • 1
    One big difference that is not clarified here is how exceptions bubble up the function call stack. You can [compare try/catch with algebraic effects](https://overreacted.io/algebraic-effects-for-the-rest-of-us/) but that bubbling feature is not something you can do with a simple data type like Maybe or Either. – Alireza Mirian Jun 24 '21 at 19:22
  • I still find it hard to see a difference against Java's Checked Exceptions. I don't think there is one, when your Monad is used to short-circuit on error, I feel it's exactly equivalent to Checked Exceptions, just with a different ergonomic. Can anyone think of another distinction in that case? – Didier A. Nov 23 '22 at 20:12
11
  1. An exception can carry more information about the source of a problem. OpenFile() can throw FileNotFound or NoPermission or TooManyDescriptors etc. A None does not carry this information.
  2. Exceptions can be used in contexts that lack return values (e.g. with constructors in languages that have them).
  3. An exception allows you to very easily send the information up the stack, without a lot of if None return None-style statements.
  4. Exception handling almost always carries higher performance impact than just returning a value.
  5. Most importantly of all, an exception and a Maybe monad have different purposes - an exception is used to signify a problem, while a Maybe isn't.

    "Nurse, if there's a patient in room 5, can you ask him to wait?"

    • Maybe monad: "Doctor, there is no patient in room 5."
    • Exception: "Doctor, there is no room 5!"

    (notice the "if" - this means the doctor is expecting a Maybe monad)

Oak
  • 5,215
  • 6
  • 28
  • 39
  • 7
    I don’t agree with points 2 and 3. In particular, 2 does not actually pose a problem in languages that use monads because those languages have conventions that do not generate the conundrum of having to handle failure during construction. Concerning 3, languages with monads have appropriate syntax for handling monads in a transparent way that does *not* require explicit checking (for instance, `None` values can just be propagated). Your point 5 is only kind of right … the question is: which situations are unambiguously exceptional? As it turns out … [not many](http://bit.ly/d2cOZ4). – Konrad Rudolph May 30 '12 at 14:28
  • 1
    @KonradRudolph regarding (2), some languages with the Maybe monad (e.g. Scala) do have constructors and other contexts in which a return value is otherwise not possible or not useful. Regarding (3) - None values can only be propagated if you don't want to actually use the value. If you do, you have to test them. And if you do test them, you find out None and you don't know how to deal with it, what do you do? Propagation makes the most sense. This is the scenario I'm trying to describe that exceptions can make much easier. I accept that it does not always apply. – Oak May 30 '12 at 14:35
  • @KonradRudolph regarding (5), I agree that the when to use what is not always obvious. I think that if you yourself catch for an exception immediately thrown from a called method, a Maybe may often be appropriate. But many times there's a noticeable stack-distance between the point where you detect a problem and the point where you decide you can deal with it, and this is where exceptions shine. Not only because if (3), but because they conceptually free a lot of methods in-between from worrying about the None option of the Maybe. i.e. it's not the doctor that has to worry about a missing room – Oak May 30 '12 at 14:38
  • 3
    Regarding (2), you can write `bind` in such a way that testing for `None` doesn’t incur a syntactic overhead however. A very simple example, C# just overloads the `Nullable` operators appropriately. No checking for `None` necessary, even when using the type. Of course the check is still done (it’s type safe), but behind the scenes and doesn’t clutter your code. The same applies in some sense to your objection to my objection to (5) but I agree that it may not always apply. – Konrad Rudolph May 30 '12 at 14:54
  • 1
    +1 for point 5! I find that often exceptions are used when in fact it would be more appropriate to use the Maybe monad (or some other way of representing an optional type). The None return value is a __valid__ return value indicating that a piece of code was executed without errors and found no valid result. An exception indicates that a piece of code encountered some error during execution. I think there is a different and it is important to make this distinction in code. – Giorgio May 30 '12 at 18:19
  • 4
    @Oak: Regarding (3), the whole point of treating `Maybe` as a monad is to make the propagating `None` implicit. This means that if you want to return `None` given `None`, you do not have to write any special code at all. The only time you need to match is if you want to do something special on `None`. You never need `if None then None` sort of statements. – Tikhon Jelvis May 30 '12 at 20:23
  • @TikhonJelvis you make a good argument. I guess well-written code should only do the test when it really needs to, just like well-written code will only catch an exception when it needs to. – Oak May 30 '12 at 20:35
  • 5
    @Oak: that's true, but that wasn't really my point. What I'm saying is that you get `null` checking exactly like that (e.g. `if Nothing then Nothing`) for *free* because `Maybe` is a monad. It's encoded in the definition of bind (`>>=`) for `Maybe`. – Tikhon Jelvis May 30 '12 at 20:45
  • 2
    Also, regarding (1): you could easily write a monad that can carry error information (e.g. `Either`) that behaves just like `Maybe`. Switching between the two is actually rather simple because `Maybe` is really just a special case of `Either`. (In Haskell, you could think of `Maybe` as `Either ()`.) – Tikhon Jelvis May 30 '12 at 20:56
6

"Maybe" is not a replacement for exceptions. Exceptions are meant to be used in exceptional cases (for instance: opening a db connection and the db server is not there although it should be). "Maybe" is for modeling a situation when you may or may not have a valid value; say you are getting a value from a dictionary for a key: it may be there or may be not - there is nothing "exceptional" about any of these outcomes.

Nemanja Trifunovic
  • 6,815
  • 1
  • 26
  • 34
  • 2
    Actually, I have quite a bit of code involving dictionaries where a missing key means stuff has gone horribly wrong at some point, most likely a bug in my code or in the code that converted the input into whatever is used at that point. –  May 30 '12 at 15:17
  • 3
    @delnan: I am not saying a missing value can't ever be a sign of an exceptional state - it just doesn't have to be. Maybe really models a situation where a variable may or may not have a valid value - think nullable types in C#. – Nemanja Trifunovic May 30 '12 at 15:30
6

I second Tikhon's answer, but I think there's a very important practical point that everybody's missing:

  1. Exception handling mechanisms, at least in mainstream languages, are intimately coupled to individual threads.
  2. The Either mechanism isn't coupled to to threads at all.

So something that we're seeing nowadays in real life is that many asynchronous programming solutions are adopting a variant of the Either-style of error handling. Consider Javascript promises, as detailed in any of these links:

The concept of promises allows you write asynchronous code like this (taken from the last link):

var greetingPromise = sayHello();
greetingPromise
    .then(addExclamation)
    .then(function (greeting) {
        console.log(greeting);    // 'hello world!!!!’
    }, function(error) {
        console.error('uh oh: ', error);   // 'uh oh: something bad happened’
    });

Basically, a promise is an object that:

  1. Represents the result of an asynchronous computation, which may or may not have been finished yet;
  2. Allows you to chain further operations to perform on its result, which will be triggered when that result is available, and whose results in turn are available as promises;
  3. Allows you to hook up a failure handler that will be invoked if the promise's computation fails. If there is no handler, then the error is propagated to later handlers in the chain.

Basically, since the language's native exception support doesn't work when your computation is happening across multiple threads, a promises implementation has to provide an error-handling mechanism, and these turn out to be monads similar to Haskell's Maybe/Either types.

sacundim
  • 4,748
  • 1
  • 19
  • 16
  • It has nothing todo with threads. JavaScript in the browser always run in one thread not on multiple threads. But you still cannot use exception because you don't know when your function is called in the future. Asynchronous doesn't automatically mean an involvement of threads. That's also the reason why you cannot work with exceptions. You only can fetch an exception if you call a function and immediately it gets executed. But the whole purpose of Asynchronous is that it runs in the future, often when something other finished, and not immediately. That's why you cannot use exceptions there. – David Raab Sep 15 '15 at 02:11
  • I am not sure these are monads; the associativity is not preserved with the example of promises. – pro100tom Jul 17 '22 at 14:46
1

The Haskell type system will require the user to acknowledge the possibility of a Nothing, whereas programming languages often don't require that an exception be caught. That means that we'll know, at compile-time, that the user has checked for an error.

chrisaycock
  • 6,655
  • 3
  • 30
  • 54
  • 1
    There are checked exceptions in Java. And people often treated even them badly. – Vladimir May 30 '12 at 13:31
  • 1
    @DairT'arg ButJava requires checks for IO errors (as an example), but not for NPEs. In Haskell it's the other way around: The null equivalent (Maybe) is checked but IO errors simply throw (unchecked) exceptions. I'm unsure how much of a difference that makes, but I don't hear Haskellers complaining about Maybe and I think a lot of people would like NPEs to be checked. –  May 30 '12 at 13:40
  • 1
    @delnan People want NPE to be checked? They must be mad. And I mean that. Making NPE checked only makes sense if you have a way of *avoiding* it in the first place (by having non-nullable references, which Java doesn’t have). – Konrad Rudolph May 30 '12 at 13:45
  • 5
    @KonradRudolph Yes, people probably don't want it to be checked in the sense that they'll have to add a `throws NPE` to every single signature and `catch(...) {throw ...} ` to every single method body. But I do believe there's a market for checked in a same sense as with Maybe: nullability is optional and tracked in the type system. –  May 30 '12 at 13:49
  • 1
    @delnan Ah, then I agree. – Konrad Rudolph May 30 '12 at 14:22
  • @delnan Just to mention, C# and some new JVM-based languages (Kotlin?) do have non-nullable references. So, it's just ancient design drawback in Java. – Vladimir May 30 '12 at 15:12
  • @DairT'arg C# doesn’t have non-nullable references. – Konrad Rudolph May 30 '12 at 15:15
  • @DairT'arg I'm aware of `Nullable` which makes value types nullable, but I haven't heard of non-nullable references in C#. Some .NET research languages (Spec# IIRC, possibly more) indeed have non-nullable references, but to the best of my knowledge, this hasn't been adapted in C# (yet). Could you give a source? –  May 30 '12 at 15:15
  • Java might not have nullable references, but you can just not use null (and check for it at module boundaries of course). – Ricky Clarkson May 31 '12 at 14:36
0

The maybe monad is basically the same as most mainstream language's use of "null means error" checking (except it requires the null to be checked), and has largely the same advantages and disadvantages.

Telastyn
  • 108,850
  • 29
  • 239
  • 365
  • 8
    Well, it does *not* have the same disadvantages since it can be statically type checked when used correctly. There is no equivalent of a null pointer exception when using maybe monads (again, assuming they are used correctly). – Konrad Rudolph May 30 '12 at 13:44
  • Indeed. How do you think that should be clarified? – Telastyn May 30 '12 at 13:58
  • @Konrad That's because in order to use the value (possibly) in the Maybe, you _must_ check for None (though of course there's the ability to automate a lot of that checking). Other languages keep that sort of thing manual (mostly for philosophical reasons I believe). – Donal Fellows May 30 '12 at 14:52
  • 2
    @Donal Yes but note that most languages which implement monads do provide appropriate syntactic sugar so that this check can often be hidden away completely. E.g. you can just add two `Maybe` numbers by writing `a + b` *without* the need to check for `None`, and the result is once again an optional value. – Konrad Rudolph May 30 '12 at 14:57
  • 2
    The comparison with nullable types holds true for the `Maybe` *type*, but using `Maybe` as a monad adds syntax sugar that allows expressing null-ish logic a lot more elegantly. – tdammers May 30 '12 at 16:33
0

Exception handling can be a real pain for factoring and testing. I know python provides nice "with" syntax that allows you to trap exceptions without the rigid "try ... catch" block. But in Java, for example, try catch blocks are big, boilerplate, either verbose or extremely verbose, and hard to break up. On top of that, Java adds all the noise around checked vs. unchecked exceptions.

If, instead, your monad catches exceptions and treats them as a property of the monadic space (instead of some processing anomaly), then you're free to mix and match functions you bind into that space regardless of what they throw or catch.

If, better yet, your monad prevents conditions where exceptions could happen (like pushing a null check into Maybe), then even better. if...then is much, much easier to factor and test than try...catch.

From what I've seen Go is taking a similar approach by specifying that each function returns (answer, error). That's sort of the same as "lifting" the function into a monad space where the core answer type is decorated with an error indication, and effectively side-stepping throwing & catching exceptions.

sea-rob
  • 6,841
  • 1
  • 24
  • 47