I’ve been learning Lisp for few years now, and every Lisp book I read keeps saying that Lisp is a flexible language that you can extend to the degree when it fits naturally to your domain. It’s easy to say, but what exactly does this phrase mean? After all, when you program in your non-Lisp language, don’t you modify it for your domain problem? I’ve been thinking about it for a long time, and only recently I started to understand what flexibility really means. There is a difference between using the language and changing the language to solve a problem. In this post I will try to show the difference based on a simple example.
Suppose you have a process that listens to a message queue. The messages are just ordinary maps. If the map contains certain keys, one or more handlers must be invoked. Here is a matrix that shows which handler is invoked for which key.
For example, if the map has key a, then DocHandler and AlertHandler need to be called. If it has key b, then NoteHandler and AlertHandler are called. In reality there might be more keys and more handlers, but for simplicity we limit our example to three keys and three handlers.
Let’s see how this can be implemented in Java. I chose Java just as an example of non-Lisp language. You can pick any other non-Lisp language instead.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
The internals of
handle methods might be very different in reality. Consider the fact they have the same structure here as a coincidence. What is not coincidence though is the structure of
is methods. Those methods are identical indeed.
Is this code clean? I would say, no. The main issue is that it’s split in three separate but closely related parts. If tomorrow I introduce another message key and a new handler, I have to change three places in the code. Another problem is the code duplication in two spots: a series of
if statements and a group of
The last thing to notice about this code is that it’s hard to see what kind of problem it’s trying to solve. If I didn’t provide a matrix which maps message keys to handlers, it would take even more time to figure out what the code is doing.
Can we make this code better? Let’s rewrite it as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
In this version we eliminated ugly
if series, and grouped together decision making logic and message handling. From that perspective the code became cleaner, but not necessarily clearer. Now it actually takes more effort to understand what the code is doing. Also, the duplication inside the
is methods is still there. We can fix it by extracting it to some abstract class or utility method. We can also use Java reflection within
handlers method to build a collection of handlers without explicitly specifying them. All these manipulations arguably make the code cleaner, but… one thing we’ll never be able to fix is the separation between decision making logic and message handling. Whatever you do, there will always be the if-statement, in one form or another, that checks if you need to process the message, and the message processing logic itself. Those two things will always be separate. This is the point where we hit the language limits.
Now let’s try to solve the same problem in Lisp and see if we can fix the language to eliminate the last issue from the paragraph above. Here is the direct translation of the previous Java snippet to Clojure dialect of Lisp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
This code is already easier to read, but we can do even better. The separation between decision making logic and message handling is still there. At this point we should ask the question: what kind of code do we want to see there? And the answer is: we want to replace the
-handler methods above with the following code
1 2 3 4 5 6 7 8
You see: no conditionals. Handlers are self-sufficient entities which know when they have to be applied and how. In their signatures they explicitly declare which message keys they expect, and in the body they just use those keys. No boilerplate: clean and simple. The beauty of Lisp is that you can actually implement that code. The way you do it is by creating a macro which generates the appropriate functions. Creating a macro is not a simple task, I spent quite some time to get this one working, but it’s worth of doing, because it makes the code clean and clear.
We can make one additional step further by moving the
handler declarations inside the
build-handlers function. (We need one small macro for that.) And here is the final solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
As I said, the first macro might be cryptic, but look at the lines 11–20. This is the essence of our problem, and it cannot be done any simpler. Suppose, we need to implement a new handler which should be called if key c is present in the message. Here what we would need to add to
build-handler’s body to implement this new requirement
Simple, right? And what if a new key d is added to the message that should be processed by document handler? Here is what we need to change
We just add a new key to the function’s parameter list. That’s it — one-word change.
Lisp is the most powerful programming language. By that I mean you can change the language in such a way that the solution to any particular problem can be expressed in the simplest possible way. By changing the language, you can remove all the barriers between the language and the problem domain. I hope I demonstrated this in my simple example.