1

Please ignore performance issues, I am interesting in data flow, safety, modelling, reasoning.

I wonder what are the limitations of exception approach to error reporting implemented like in Java compared to Haskell-like (OCaml, and Rust use same/similar approach AFAIK).

As far as I know in both cases error is part of the function signature, but in Haskell you have to explicitly "handle" the error even if entire "handling" is just passing it further. I see a difference for the reader, in Java:

try
{
  a = foo();
  b = bar();
}

it is impossible to tell (just by looking at the code) if foo and/or bar can end up with error, also in both cases the error can be silently passed. If I understand correctly in Haskell the the counterpart code would be put into monad:

do a <- foo();
   b = bar();

(I am new to Haskell, so forgive me the errors) and it is clear where the drop occurs and which line can fail.

BUT -- this is for human, compiler knows it in both cases.

So when it comes to, say, reasoning about the data flow (for compiler, not human), what are the limitations of Java approach? In other words, what is not doable in Java that is possible in Haskell? Or what information Haskell has which is not present in Java?

greenoldman
  • 1,533

2 Answers2

2

I think the most interesting difference between the two, certainly from the perspective of a language designer, is that Haskell's error handling is not a language feature but is part of the library, and yet it manages to be just as expressive and easy to work with as Java's. The reason that this works so nicely is due to Haskell's widespread use of Monads, which allow two useful properties:

  • the code implementing the Monad type is passed blocks of code (i.e. functions that operate on the value(s) stored in the Monad and return a new Monad instance, which are conceptually comparible to the notion of either a code block or a statement in most imperative languages), and is allowed to call those one, none, or indeed as many times as it wants. In any order it wants (cf the continuation monad). This means that user level code can effectively implement just about any flow control pattern it needs.

  • the language provides easy-to-use syntax sugar that makes writing code using Monads particularly easy.

Another interesting feature of Haskell's reliance on a user-supplied data type for handling errors is that if you write data-type generic code, then the caller of that code can dictate how the errors are handled. For example, if I write a function with a signature like this:

myUsefulFunction :: Monad m => String -> String -> m String

I can call this function in a variety of ways... for example, using maybe defaultString id (myUsefulFunction str1 str2), which will give the result of the function on success or a default if it failed, or using runError (myUsefulFunction str1 str2) which will return an Either containing the error message or the result. This allows more choice in how to structure my code, allowing me to produce a better result with less boilerplate code (no catching exceptions and substituting a default value, for instance).

Jules
  • 17,880
  • 2
  • 38
  • 65
1

As @Jules points out, Haskell's monads can be implemented as a library, whereas Java's exceptions are built into the language.

This has implications in practice: With Monads in Haskell, you can abstract over any function regardless of the errors, whereas in with checked exceptions in Java you can't.

For example, you can pass a function returning an Either to map:

parseURL: string -> Either string URL
map parseURL ["https://stackexchange.com", "lorem ipsum"]
-- returns [Right (URL "https://stachexchange.com"), Left "invalid url"]

It works because an Either is just a value that encodes failure, not a special language construct for failures. And you can return any value from a function passed to map.

In Java, however, this doesn't work:

Stream.of("https://stackexchange.com", "lorem ipsum")
      .map(URI::new)

While you can also return any value from a function passed to map, you cannot throw checked exceptions. Stream::map takes a Function, a functional interface whose apply method doesn't declare any checked exceptions to be thrown. URI::new, on the other hand, throws an InvalidURISyntaxException, so it cannot be used as a Function.

There are several workarounds, but clearly Haskell wins here in terms of elegance.

Also, note how the results are collected into a list, so we can get inspect multiple failures at once.

mperktold
  • 151