Boolean operations like AND and OR have short-circuiting behavior in most languages. This means the second argument is evaluated only if the value of the expression is not already determined by the first argument. A short-circuiting operator can prevent the evaluation of some expensive computation, by checking for some preconditions. This is illustrated in the following C code:

if (run_expensive_computation && expensive()) {
  printf("expensive computation executed\n")
}

In this example, the function expensive() will not be called if run_expensive_computation is set to 0.

A short-circuiting operator can prevent runtime errors by checking for preconditions required for evaluating an expression. This is demonstrated in the following program:

if (x != 0 && num / x) // ensures a division-by-zero runtime error does not occur
{
  // ...
}

In most Lisps, including Clojure, the macros and and or implement these short-circuiting operators. Here is the and macro in action:

user> (and (= 20 20) (> 20 10) (< 20 50))
true
user> (and (= 20 20) (> 20 100) (< 20 50))
false
user> (and)
true
user> (and 1 2 3)
3

and will stop evaluating at the first argument that returns a logical false (false or nil). In contrast, or will evaluate its arguments until the first expression that returns a logical true value:

user> (or (do (println 1) (= 2 2)) (do (println 2) (> 2 1)))
;;-> 1
true
user> (and (do (println 1) (= 2 2)) (do (println 2) (> 2 1)))
;;-> 1
;;-> 2
true

Defining Our Own Short-circuiting Operator

If you are not familiar with languages of the Lisp family, this may come as a surprise - you can extend the language by defining your own short-circuiting operators! This is achieved with the help of the macro system.

Macros are functions that are evaluated at compile time and return Lisp code that is further compiled by the compiler. This is possible because Lisp code is represented as data. Lisp represent code as parenthesized lists (s-expressions). In the case of Clojure the encoding is a bit more elaborate.

The point is, representing code as data enables the existence of a macro system powerful enough to add low-level operators to the language.

The new operator that we are going to define performs a simple but useful task - it allows us to handle errors in a functional way.

Ideally we would like functions to return values and not perform any side-effect, like throwing an exception. On success, a function may return a map {:ok success_value} and on failure it may return {:error failure_value}.

The problem with this approach is that each call site has to be wrapped in a conditional expression as in:

(let [r (f)]
  (if (:ok r)
    (:ok r)
    (handle-error (:error r))))

We would like to have an operator that will execute a sequence of expressions for us. If any of those expressions return an {:error ...}, the operator should immediately break out of the sequence. None of the remaining expressions should be evaluated.

The following macro defines this operator, which we call either because it will eventually return a success or failure value, no matter how many expressions are evaluated:

(defmacro either
  ([] nil)
  ([expr] expr)
  ([expr & exprs]
   `(let [r# ~expr]
      (if (:error r#)
        r#
        (either ~@exprs)))))

The macro can be called with no-arguments, a single argument or an arbitrary number of arguments. All these cases are handled separately. The first two cases are trivial to handle.

For the third case, the macro emits code that checks if the value of the first expression is {:error ...}. If this is the case, the macro just returns the error and terminates evaluation. Otherwise, it recurses to evaluate the rest of the arguments.

Let us check if our short-circuiting operator really does what it is supposed to do:

user> (defn f [] (println "f") {:ok 100})
user> (defn g [] (println "g") {:error 200})
user> (defn h [] (println "h") {:ok 300})
user> (either (f) (h))
;;-> f
;;-> h
{:ok 300}
user> (either (f) (g) (h))
;;-> f
;;-> g
{:error 200}

The last call to either did not evaluate (h) because (g) failed with an error. The operator is really short-circuiting!

You might have noticed that either only returns the value of the final expression that succeeded (or failed). How can we make use of all success values that were returned?

One possible extension to either is to make it pass the last success value to the next expression. (This means, all the expressions except the first one should be functions). You may want to implement this extension as an exercise.

All the best!


Note that name and e-mail are required for posting comments