Search

Error Handling

생성일
2021/07/30 03:36
태그

Functional Error Handling

When dealing with errors in a purely functional way, we try as much as we can to avoid exceptions. Exceptions break referential transparency and lead to bugs when callers are unaware that they may happen until it's too late at runtime.
In the following example, we are going to model a basic program and go over the different options we have for dealing with errors in Arrow.

Requirements

Take food out the refrigerator
Get your cutting tool
Cut up the lettuce to make lunch

Requirements

Exceptions

A naive implementation that uses exceptions may look like this
As you may have noticed, the function signatures include no clue that, when asking for takeFoodFromRefrigerator() or getKnife(), an exception may be thrown.

The issues with exceptions

Exceptions can be seen as GOTO statement, given they interrupt the program flow by jumping back to the caller. Exceptions are not consistent, as throwing an exception may not survive async boundaries; that is to say that one can't rely on exceptions for error handling in asynchronous code, since invoking a function that is async inside a try/catch may not capture the exception potentially thrown in a different thread.
Because of the extreme power of stopping computation and jumping to other ares, Exceptions have been abused in core libraries to signal events.
They often lead to incorrect and dangerous code because Throwable is an open hierarchy where you may catch more than you originally intended to.
Furthermore, exceptions are costly to create. Throwable#fillInStackTrace attempts to gather all stack information to present you with a meaningful stacktrace.
Constructing an exception may be as costly as your current Thread stack size, and it's also platform dependents since fillInStackTrace calls into native code.
More info on the cost of instantiating Throwable, and throwing exceptions in general, can be found in the links below.
Exceptions may be considered generally a poor choice in Functional Programming when:
Modeling absence
Modeling known business cases that result in alternate paths
Used in async boundaries voer APIs based callbacks that lack some from of structured concurrency.
In general, when people have no access to your source code.

How do we model exceptional cases then?

Arrow and the Kotlin standard library provides proper datatypes and abstractions to present exceptional cases.

Nullable types

We use Nullable types to model the potential absence of a value.
When using Nullable types, our previous example may look like:
It's easy to work with Nullable types if your lang supports special syntax like ? as Kotlin does. Nullable types are faster than boxed types like Option. Nonetheless Option is also supported by Arrow to interop with Java based libraries that use null as signal or interruption value like ReactiveX RxJava. Additionally Option is useful in generic code when not constraining bonds of A : Any and using null as a nested signal to produce values of Option<Option<A>> since A? can't have double nesting.
In addition to let provided by the standard library Arrow provides nullable which allows the use of Computation Expressions.
While we could model this problem using Nullable Types, and forgetting about exception, we are still unable to determine the reasons why takeFoodFromRefrigerator() and getKnife() returned empty values in the form of null. For this reason, using Nullable Types is only a good idea when we know that values may be absent, but we don't really care the reason why. Additionally, Nullable Types are unable to capture exceptions. If an exception was thrown internally, it would still bubble up and result in a runtime exception.
In the next example, we are going to use Either to deal with potentially thrown exceptions that are outside the control of the caller.

Either

When dealing with a know alternate path, we model return types as Either. Either represents the presence of either a Left value or a Right value. By convention, most functional programming libraries choose Left as the exceptional case and Right as the success value.
It turns out that all exceptions thrown in our example are actually known to the system, so there is no point in modeling these exceptional cases as java.lang.Exception.
We can now assign proper types and values to the exceptional cases.
This type of definition is commonly known as an Algebraic Data Type or Sum Type in most FP capable languages. In Kotlin, it is encoded using sealed hierarchies. We can think of sealed hierarchies as a declaration of a type and all its possible construction states.
Once we have an ADT defined to model our know error, we can redefine our functions.
Arrow also provides an Effect instance for Either in the same way it did for Nullable Types. Except for the types signature, our program