Posterous theme by Cory Watilo

Runtime Dynamism + Performance: Have Your Cake and Eat It Too

It's common in software development to want to be able express cross-cutting concerns. It's also often desirable to to express these cross-cutting concerns at runtime. Sadly many languages do not allow these concerns to be expressed at all! You can forget about at runtime. One of the great appeals of dynamic programming languages is freedom from of these kinds of shackles. Yet most of the popular dynamic programming languages (JavaScript, Python, Ruby, etc.) are only able to deliver this promise in a crippled way; they all require the user to engage in some form of monkey-patching.

In addition to these limitations programmers are also willing to accept the slow performance of these languages simply because they afford far more productivity. And often the most important factor in a project is how quickly and happily the developer can "run" - the performance of the software can be solved cheaply with faster hardware, more hardware, caching, sophisticated databases, etc. Yet time and time again we see projects migrate away from popular dynamic languages to popular static ones. Developers become depressed, ill-tempered, and generally unethusiastic.

All the fun is gone! Will we never be able to have our cake and eat it too!!!

Fascinatingly enough, Clojure has a novel solution to this common problem that not many people are familiar with.

Let's begin with a concrete example. Sequences are deeply ingrained into Clojure's design - any object that implements the ISeq interface can be treated generically as a sequence in Clojure code. However some types such as Clojure's nifty PersistentVectors are not sequences, rather they implement the Seqable interface. That means if you call seq on them, it is possible to treat them as sequences. But there are a lot of things one might wish to treat as a sequence- String, Iterable, CharSequence, java.util.Map, etc. In fact Clojure supports these. However there is no current functionality to query if an instance is actually seq-able according to Clojure. To fill the hole of this missing functionality say we define a function called seqable? that is able to take any x and tell us if we can call seq on it. We could do something like the following:

However this is clearly undesirable. This is a hardcoded list of what things are or are not seq-able! Even worse what about the massive set of Java primitive arrays?!

How does this perform?

This averages around 8 seconds which is criminally slow. This is becase we have to use reflection to determine if an object is a Java Array. At this point you shake your fist at the heavens and curse the JVM. But all is not lost! Clojure includes the required machinery to elegantly express cross-cutting concerns - protocols. We can write something like the following:

However you'll notice this doesn't handle the Java Array case. If we want to express a default we can extend Object to our protocol like so:

Something very interesting is happening here. If a handled Object is discovered to be an array at runtime, extend its type to ISeqable! This system evolves over time as it receives various kinds of input! And it is important to note that this system is open. That is, anyone can come along and add another type to this list of types that should respond true to seqable?.

But what does this kind of dynamism mean for performance? (Often I've turned away from interesting dynamic languages that purport to explore expressivity while exhibiting performance that is so miserable as to be nearly comical)

The above averages around 600ms on my machine. If this doesn't seem futuristic to you then I don't know what will :)

Thanks to chouser and mec from the Clojure IRC channel for the conversation that inspired this post.

‚Äč