Nature, to Be Commanded, Must Be Obeyed

March 27, 2014

A Makeshift Solution to Expression Problem

In the previous post
I tried to demonstrate the expression problem and its implications. In this post
I will present a solution. As I mentioned last time I interpret the static
type safety clause as no monkeypatching. Otherwise there is no solution for
expression problem in Clojure, since it’s a dynamic langulage with no static
type checking.

Let me remind you the little shape language we’ll be using in the examples
again:

Notice that none of these are now function definitions. So what do defcase
& deffunction do? We’ll come to that. I think the first question we should
ask is where are the particular implementations of area for circles and
squares? OK, I lied to you, we need to add three new constructs, not two:

(area(make-circle1)); => IllegalArgumentException: area is not defined for case circle(area(make-square2)); => IllegalArgumentException: area is not defined for case square(defimplementationarea[circle](* Math/PI(:radiuscircle)(:radiuscircle)))(defimplementationarea[square](* (:edgesquare)(:edgesquare)))(area(make-circle1)); => 3.141592...(area(make-square2)); => 4

I guess you have realized by now that defcase, deffunction &
defimplementation are all macros. Since macros are not the easiest things
to understand I wanted to show their usage before revealing their code.

defimplementation

Did you notice above, area had thrown an IllegalArgumentException when
we called it before we defined an implementation? But then it worked once we
supplied the implementation. Clearly there’s some hidden state.
defimplementation must be storing[1] the implementation of area for
the given case somewhere:

If you are not familiar with Clojure, here is an annotated version of the
important form in this macro:

;; Change the value of atom named 'functions' atomically...(swap!functions;; ...by associating...assoc;; ...the key, that is a vector whose first element is a;; keyword (similar to an interned string) of the name of;; the function (:area in our example) and the second;; element is a keyword of the name of the case...;; (:circle or :square in our example)[(keyword function-name)(keyword arg)];; ...with the a function that takes an argument whose;; name is whatever is 'arg' set to, containing instructions;; that are passed to the macro with its 'body' argument.(eval `(fn [~arg]~body)))

We we define the implementations of each function for each case using
defimplementation and if an implementation is not found for a given
(function × case) pair an IllegalArgumentException will be raised.

Before we move onto the other macros, let’s take a look at functions.
Nothing special really[2]:

(def functions(atom{}));; After we run the two defimplementation calls:(= (keys (deref functions))'([:area:circle][:area:square])); => true

deffunction is what ties it all. Remember how we built the keys of the
functions map? Once we have this tuple of function name and case name, we
can get the corresponding value, which is a function object, from functions
and all that is left to do is to call it with the shape given:

Perhaps looking at what deffunction expands to helps understanding it:

(macroexpand '(deffunctionarea[shape]));; Produces something like:(def area(fn [shape](let [case-key_(:caseshape)default_(fn [arg2__351__auto__](throw(IllegalArgumentException.(str "area"" is not defined for case "(name case-key_)))))impl-key_[:areacase-key_]impl-fn_(get @functionsimpl-key_default_)](impl-fn_shape))));; If you were to call this function as below:(area(make-circle1));; It could be substituted with the following form:((get @functions[:area:circle](fn [arg2]...))(make-circle1))

This is all the machinery we need to support open extension. Remember; the
consumers don’t need to know the implementation details of defcase,
deffunction and defimplementation. They don’t need to know even the
existence of functions. From their perspective they would add cases and
functions without modifying existing code and it would just work.

Adding New Functions

Let’s add a new function that calculates the circumference/perimeter of a shape:

Conclusion

My solution works but it shouldn’t be used in production. Clojure’s answer to
expression problem is multimethods and protocols. My makeshift solution is,
hopefully, only good for educational purposes.

It would be nice be able to know if a case is applicable to a function at
compile time. But it would be an unreasonable expectation from a dynamic
language. This is what tests are for, they are executable specifications of
your code. But I digress. If you need compile time guarantees you can use a
statically typed langulage like Haskell or Scala. Actually I have seen a method
that seems to satisfy all the conditions of expression problem. But it was
quite complex:

complex

(adj) consisting of many different and connected parts.

The reason why I chose Clojure for this post (and the previous one) is because
it’s a language designed to keep complexity under control. Open extension,
for example, is built into the language.

That’s it from me. I won’t go into the details of multimethods or protocols,
you can easily find out about them.

This is probably not a great use of macros. I’m not an expert but as far
as I know macros should return forms instead of causing side effects.
Actually it is possible to write defimplementation as a function but
it wouldn’t have been as nice looking. In any case consumers of this
code shouldn’t worry about the internals.

I used an atom but I suppose a var would do here as well. Since
cases and functions are unlikely to be defined run-time functions
will only be modified at startup. The important point here is that it
should be somewhat hidden. Smoke and mirrors.

If you have any questions, suggestions or corrections feel free to drop me a line.