Eventual Exceptions vs Programming in a Minimal Functional Style

posted by Craig Gidney on May 22, 2013

One of the downsides to exceptions that I’m becoming more sensitive to as time passes, at least as they are implemented in most languages (python, ruby, C#, C++, Java, VB, JavaScript, etc), is mixing poorly with a functional programming style.

In this post, I’ll explore this issue from the perspective of eventual results / promises / futures.

Representing Eventual Results

The standard promise/future/eventual-value type in .Net is called Task<T>.

Like all decent ‘eventual result’ types, Task<T> supports or can support all the operations needed to qualify as a monad (wrapping, transforming, flattening):

// wrapping
Task<int> wrapped = Task.FromResult(10);

Task<bool> wrapped2 = Task.FromResult(true);

// transforming
Task<IPAddress> transformed = from hostEntry in Dns.GetHostEntryAsync("example.com")
                              select hostEntry.AddressList.First();

Task<Task<TcpClient>> transformed2 = from address in projected
                                     select Tcp.ConnectAsync(address, 80);

// flattening
Task<TcpClient> flattened = transformed2.Unwrap();

However, Task<T> is more complicated than strictly necessary. A simplest-possible eventual result, a MinimalTask<T>, would only be able to represent eventually succeeding with a value of type T. A Task<T>, on the other hand, can succeed with a value of type T, or fail with an exception, or even end up cancelled with no associated value. When you ask for a Task<T> you’re actually asking for a MinimalTask<MayFailWithException<MayBeCancelled<T>>>!

Of course, there are good practical reasons for Task<T> to be able to represent failure and cancellation. If you try to run a function asynchronously, and it throws an exception, it’s downright useful to have a place to put that exception. Most operations can fail. Most asynchronous operations can be cancelled. It makes sense to support those scenarios.

Notice that .Net having exceptions just leaked into how we represent eventual results.

All tasks can fail or be cancelled. That means everything that works with tasks must handle cancellation and faulting in an appropriate way. The flexibility is useful, but when it isn’t needed it unnecessarily complicates things and introduces opportunities for bugs. (Note similarities to the downsides of forcing all references to allow a null value.)

A good example of the trade-offs due to cancellable faultable tasks is the design of Task.WhenAll.

Aggregating Eventual Results

Task.WhenAll takes a sequence of tasks and returns a task that completes when all of the tasks have completed. More exactly, once all of the tasks have completed, they are combined as follows. If any of the tasks failed, the resulting task fails with an aggregate exception containing all of the failures. Otherwise, if any of the tasks were cancelled, the resulting task ends up cancelled. Otherwise the resulting task succeeds with an array of the tasks’ values.

Notice that, if tasks couldn’t represent exceptions, the above description would be a lot simpler. It wouldn’t discuss how to aggregate exceptions, it wouldn’t note that exceptions take priority over cancellations, etc. The majority of the method’s specification is made up of details that aren’t directly related to eventual-ness, and could exist elsewhere.

A hypothetical WhenAll method for a hypothetical MinimalTask<T> type would be simpler than Task.WhenAll. Since there’s no exceptions or cancellations, there’s no need to specify the details of combining exceptions, whether exceptions take priority over cancellation, and etc. MinimalTask.WhenAll “flips” the order of many-ness and eventual-ness in the obvious way, and that’s it.

Can we implement Task.WhenAll in terms of the hypothetical MinimalTask.WhenAll? Keeping in mind that a Task<T> is essentially a MinimalTask<MayFail<May<T>>>, it’s actually pretty easy. All of the details of how to aggregate should already be in the types designed to represent potential-failure and potential-cancellation, so the method ends up being downright simple:


MinimalTask<MayFail<May<T[]>>> WhenAllMay(this IEnumerable<MinimalTask<MayFail<May<T>>>> tasks) {
    return from v1 in MinimalTask.WhenAll(tasks) // v1: MayFail<May<T>>[]
           from v2 in MayFail.WhenAll(v1)        // v2: May<T>[]
           from v3 in May.WhenAll(v2)            // v3: T[]
           select v3;                            // result: T[] in May in MayFail in MinimalTask
}

(Interesting fact: actually this method is in a sense not even necessary because all WhenAll methods can be implemented with the same code.)

By separating eventual-ness from may-cancel-ness and may-fail-ness, we avoid repeating the details of how to aggregate exceptions and cancellations.

Wrapping Up

The technical term for a type that represents ‘is a result of type T or else a failure’ is “Error Monad” (it’s a monad because you can wrap, transform, and flatten may-have-failed-ness).

[Aside: I can't believe the best 'error monad' link I could find was technical Haskell documentation. If anyone has a more informative link, I would appreciate it.] (old link, new link suggested by DR6)

Unfortunately, although it may be aesthetically pleasing and proper functional style to separate may-fail-ness from eventual-ness, I’m not sure if it’s a good idea or not in imperative languages with exceptions. The problem is that they already have a way to represent failure, and the easiest way to fight your language is to add second way to do something included “in the box”.

Summary

Because .Net has exceptions, Task<T> is forced to represent exceptional results. Because Task<T> can represent exceptional results, async methods are often more complicated than necessary (making them harder to work with, functionally). Separating the ‘might fail’ part out of Task<T> and into its own type is one way to avoid the complications when they aren’t necessary. However, because .Net has exceptions, doing so is potentially bad form.

That’s why I consider tasks to be an example of what I mean when I say exceptions mix poorly with a functional style. They add complications to otherwise-simple functional types.

Discuss on Reddit


Twisted Oak Studios offers consulting and development on high-tech interactive projects. Check out our portfolio, or Give us a shout if you have anything you think some really rad engineers should help you with.

Archive

More interesting posts (14 of 24 articles)

Or check out our Portfolio.