Functional Programming in Java, Second Edition: Chapter 10 - Much too hard on exceptions, some thoughts

Just some thoughts on being hard on exceptions in functional languages.

On p. 167 we read:

Functional style code is amazing, concise, less complex, and easy to work with…until we hit exception handling. Exception handling is fundamentally an imperative style of programming idea. Throwing exceptions is incompatible with functional programming.

I beg to differ. There seems to be a strong feeling about this as the affirmation. It may be ugly but it’s not fundamentally a problem

Exception handling is fundamentally an imperative style of programming idea. Exception handling and functional programming are incompatible

is repeated on page 171. But in fact, a functional language - which in principle has no side effect - would have rather clean semantics regarding exceptions, would it not: just dump all the ongoing computation made since the nearest relevant “catch”. There would not even be a need to worry about anything else, like cleanup through finally blocks. As data structures in functional languages are supposed to be immutable, one would not even have to worry about such structures being left in some intermediate, improper state.

Furthermore we read:

You are not allowed to throw a checked exception from the functional pipeline.

But this is a problem of how functional pipelines have been defined in Java - the syntax does not allow it. It is not a fundamental problem. The fact that one is allowed to throw unchecked exceptions as normally (in fact, that cannot be avoided) indicates as much.

Here is some very interesting commentary on this:

We read at the above:

The simple answer to your question is: You can’t [throw checked exceptions from a stream pipeline], at least not directly. And it’s not your fault. Oracle messed it up. They cling on the concept of checked exceptions, but inconsistently forgot to take care of checked exceptions when designing the functional interfaces, streams, lambda etc. That’s all grist to the mill of experts like Robert C. Martin who call checked exceptions a failed experiment.

Going further in the book on p.167:

Don’t throw a runtime exception either. Sure, the compiler does not stop you from doing so, but you’ll abruptly end the functional pipeline processing and the previously processed data may be lost.

And on p.168:

The critical question we have to ask is what happens when an exception is thrown in the middle of processing a list of IATA codes. Exceptions blow up the call stack—plain and simple. If you carefully examine the code, you’ll see we are not catching the exception anywhere. So, it will result in abrupt termination of the program.

But that is the whole idea. You do want to discard (rather than blow up) all the contexts of the call stack that have become invalidated by the exception: everything up to the catch. It happens in all cases where one “throws”, whether in a stream pipeline or elsewhere. The one problem is that there is no nice place to set up “finally” handlers in a pipeline, but then again, resources should be opened & closed around the pipeline, not inside.

Examples:

ML has exceptions:

https://courses.cs.washington.edu/courses/cse341/04wi/lectures/10-ml-exceptions.html

Clojure has exceptions, they are exactly the JVM exceptions of course (probably slight different in ClojureScript, but I never looked into this)

https://clojuredocs.org/clojure.core/try

Scheme has exceptions

https://courses.cs.washington.edu/courses/cse341/04wi/lectures/15-scheme-continuations.html

Even languages with an unusual (but very powerful) model of computation like Prolog can have exceptions (although not in the - quite ancient - ISO Prolog standard), here is SWI-Prolog:

https://www.swi-prolog.org/pldoc/man?predicate=throw/1

As long as there is a concept of “a tree of computations” than can be torn down an rolled back, you can have them but: the meaning of “throwing an exception” may mesh inelegantly with the language and lead to various restrictions. And one always has to handle the two aspects in one’s head: what the program computes and how it is actually evaluated by the machine, virtual or otherwise. One could look at this as another example of a “leaky abstraction”: the state machine embedded in a real, failing world, peeks through the abstraction of a functional language exisiting in an idealistic universe layered on top.

The Monadic approach

When applied to stream pipelines, this is “Dealing with it Downstream”

The “Monadic” approach seems to be exactly the one promoted in Chapter 10. Here we create a construct similar to Java’s Optional<T> carrying either the result of a computation or information about the exceptional condition that occurred. On page 173, it is said that “we need a Union Type”, which is looking at the problem from the implementation side of course. We need an Exceptional<T>, here called Try<T>.

If a stage in the pipeline receives a Failure instead of receiving a Success, the function in that stage will merely pass along that failure downstream without processing.

In other words, applying a function to an instance of type Try<T> behaves like the identity.

Haskell seems to handle exceptions “the monadic way”

https://wiki.haskell.org/Exception

In

Govindarajan, R. (1993). Exception handlers in functional programming languages. IEEE Transactions on Software Engineering, 19(8), 826–834. doi:10.1109/32.238585

which promotes the same idea (“a shielded value”) but also adds “cure handlers” to manage locally-fixable problem, I found this text:

A key issue in program construction is robustness. Software reliability can be achieved by the judicious use of fault-tolerant tools. Exception handling is one of the two techniques used for developing reliable software.

Bretz and Ebert identify two problems in supporting exception handling constructs in functional programming. An exception, in imperative languages, is treated as a means of effecting a control transfer. Hence, there is a fundamental conflict between the functional approach followed in functional languages and the control flow-oriented view of exceptions.

Due to this, exceptions in functional languages can result in nondeterministic behavior of the FP program. For example, if there are two exception points inside a given function, the result of parallel evaluation of the function could be different, depending on which exception is raised first. This problem has been considered as intrinsic to incorporating exceptions in functional languages. Languages ML and ALEX circumvent this problem by proposing sequential execution for FP. Such a restriction is severe and is essentially required because the control flow view of exceptions has been carried through to functional languages. We discard this view and define the semantics of FP functions operating on exception objects without imposing any restriction on the execution.

Secondly, exception handling might cause side effects in expressions and hence might violate the property of referential transparency. Proposed solutions suggest the association of an environment with the functions. But in our case, discarding the conventional control flow view of exception solves this problem naturally.

Reeves et al. point out that embedding exception handling constructs in lazy functional languages can transform nonstrict functions into hyper-strict functions. (…) This is because both subexpressions need to be evaluated to determine whether or not they raise any exception. Reeves et al. claim the transformation of nonstrict actors into hyper-strict actors is due to the up-propagation of signals through nonstrict operators. To overcome this problem, the notion of down-propagation and firewalls has been defined (…). In this paper, however, we argue that it is not the up-propagation, but the persistent nature of exception values that causes the above problem.

As previously mentioned, Philip Wadler on this in a quite mathematical notation to express M<T> in “Monads for Functional Programming”, which can be found here:

https://homepages.inf.ed.ac.uk/wadler/topics/monads.html