I've been building a small application for some internal operations tasks recently and made the decision to use F#. Internal tools are a great way to sneak a little something different into a homogeneous one-stack team; they're low risk and can show off different ways of doing things to other developers. Working on a non-trivial application brings out all kinds of challenges that you don't consider when learning or experimenting in a language. One I ran into recently was error handling.
The naive approach is to have some kind of error signal, perhaps returning a dummy value like
null, or a tuple of some description, but these simply push the responsibilty of dealing with issues up to the caller. Exceptions are how errors are handled in most modern languages. Unfortunately, they're not very functional&emdash;they are a side-effect, one which breaks the promise of a function's signature and often requires additional syntax to deal with. This also makes composing functions that may fail quite difficult. Instead of ignoring the incredibly expressive type system, can it be harnessed for a better solution?
Well, how are possible failures handled in functional languages? Finding an item in a list or map is a good parallel and, in F#, there is
List.tryFind :: ('T -> bool) -> 'T list -> 'T option. Unfortunately,
Option doesn't give much information about why things went wrong and there was nothing returned. Of course, this is perfectly acceptable for a simple operation like searching a list but something more flexible is required for more general cases.
The task here is to model a function that may return a valid result, or may fail. For simplicity, assume that any error information can be contained within a simple string. It can always be extended to a more complex type if needed. All that's necessary to satisfy these requirements is a tiny union type.
type Result<'a> = | Result of 'a | Error of string
An implementation in a simple HTTP API might look like this:
let getCustomer id = match Customer.getById id with | Error message -> (500, message) | Result customer -> (200, serialise customer) let postCustomer id body = match parseCustomer body with | Error message -> (401, message) | Result customer -> match saveCustomer customer with | Error message -> (500, message) | Result () -> (204, "")
let (>>=) result f = match result with | Error message -> Error message | Result value -> f value let placeOrder customer = getBasket customer >>= checkStock >>= takePayment >>= persistOrder
What this bind operator does is, given an error, it simply returns that error, but when given a valid result, the value inside that result is passed to the function provided. This allows a chain of functions to be created such that when any of the functions returns an error execution of all subsequent functions is aborted. Errors immediately break the chain and are bubbled up for the caller to handle. In this example, that means that no customer will be charged for attempting to purchase unavailable items. By encoding this into the type system, it not only documents that there is a possibility that certain operations may fail, but forces callers to deal with this eventuality.