A Lazy Sequence

An error monad in Clojure

This brief post shows a possible error handling monad and associated ‘attempt-all’ form for Clojure. This was devised to improve the error handling in necessary evil. This assumes that you are familiar with the basics of Konrad Hinsen’s clojure.contrib.monads API and the basic use of macros and protocols in clojure.

The reason I chose to use the monad library for this is that it makes creating extensible binding forms very simple. In this piece of code, we will be creating a binding form that provides behavior similar to if-let but for an arbitrary number of clauses. In addition, it also supports rich error conditions rather than just nil.

Before we dive into this in detail, lets look at how we could create a maybe form that mirrors if-let using the maybe-m monad. maybe-m will evaluate a set of bindings and finally return a result if none of the bindings result in nil; this is very similar to if-let1:

(use '[clojure.contrib.monads :only [maybe-m]])

(defmacro maybe 
    ([bindings return] 
        `(domonad maybe-m ~bindings ~return))
    ([bindings return else] 
        `(let [result# (maybe ~bindings ~return)]
            (if (nil? result#)
                ~else
                result#))))

As a bonus, by implementing a trivial multi-arity macro, this doubles as when-let-all as well as if-let-all.

This is a great improvement over nested if-lets, but it falls down if we need to know something about the failure case. This is something that nested if-lets does provide.

Types of Failures

To extend the maybe macro to provide more information than ‘it failed’ I am going to create a failure type; very simply:

(defrecord Failure [message])

(defn fail [message] (Failure. message))

This has a utility function so that consuming code can claim failure without needing to import the Failure record. Note that because this needs to take an argument, it cannot be m-zero.

Next, I want to define a protocol to tell me if some value is considered a failure:

(defprotocol ComputationFailed
  "A protocol that determines if a computation has resulted in a failure.
   This allows the definition of what constitutes a failure to be extended
   to new types by the consumer."
  (has-failed? [self]))

(extend-protocol ComputationFailed
  Object
  (has-failed? [self] false)

  Failure
  (has-failed? [self] true)

  Exception 
  (has-failed? [self] true))

Obviously a Failure is a failure, but an Exception also is. This allows code to return exceptions (as apposed to throwing them) and have them flow through the computation like any other failure. Using a protocol here also means that if other types are considered failure conditions in consuming code (in necessary-evil this includes the Fault record type), then the consuming code can define additional rules for those cases. Unfortunately this does mean that failure types are defined for the whole application.

The Monad

Now that we have our failure conditions managed, it is time to implement the monad. This is a trivial extension of the maybe-m implementation that instead of checking for nil?, checks to see if the computation has-failed?:

(use '[clojure.contrib.monads :only [defmonad domonad]])

(defmonad error-m 
  [m-result identity
   m-bind   (fn [m f] (if (has-failed? m)
                       m
                       (f m)))])

There are only two notes I would like to make here. Firstly, I opted to not catch exceptions in the else block of the if. This allows exceptions to bypass the rest of the monadic behavior. If you wish to capture an exception and introduce it to the monadic context, you can still do that manually. Secondly I have opted not to implement an m-zero or m-plus at this time as it is beyond the scope of my needs.

attempt-all

Lastly we need to modify the maybe macro introduced above to use error-m instead:

(defmacro attempt-all 
  ([bindings return] `(domonad error-m ~bindings ~return))
  ([bindings return else]
     `(let [result# (attempt-all ~bindings ~return)]
        (if (has-failed? result#) 
            ~else
            result#))))

The usage of this form is what you would expect:

(attempt-all [a 1
              b (inc a)]
              b
              "failed")             ; -> 2

(attempt-all [a (fail "an error")
              b (inc a)]
              b
              "failed")             ; -> "failed"

(attempt-all [a (fail "an error")
              b (inc a)]
              b)                    ; -> #:user.Failure{:message "an error"}

Footnotes

  1. Also worth noting here are clojure.contrib.core/-?> and clojure.contrib.core/-?>> which are the nil checking version of -> and ->>; This makes them related to the maybe form described here, but without the named values.