Writing portable code by dispatching on implementation

In the post on sorting functions by their sizes in assembly, a function was written to read a non-standardised output string. That function was made specifically to work with Steel Bank Common Lisp, which makes it non-portable.

This has to be the dumbest joke I’ve put on this site

In that article though, I touched on generic functions. Normally generic functions dispatch by doing equality checks on the type of the args given, with #’eql. We can override this to an extent, providing our own equality-check predicates, but only using #eql.

What this implies though is that it’s possible to write several versions of a generic function, and CLOS will call the function written specifically for the currently used implementation of Lisp. It’s quite simple really, though the wrapper I’ve made to demonstrate it isn’t very pretty. A macro could be written to beautify it.

Here is a hypothetical function, #”my-nonportable-func, written in such a way as to only use features which are standard among all Common Lisp implementations.

(defmethod my-nonportable-func (value implementation)
  "running my-nonportable-func using only standardised features")

And here is that same function, rewritten to use features specific to the SBCL implementation, and then the cLisp implementation:

(defmethod my-nonportable-func (value (implementation (eql 'SBCL)))
  "running my-nonportable-func with SBCL specific features")

(defmethod my-nonportable-func (value (implementation (eql (intern "cLisp"))))
  "running my-nonportable-func using features specific to cLisp")

#’eql compares objects by their identity. This means that we can’t rely on (eql “A” “A”) returning true. Instead, we intern the string such that we’re doing a symbol comparison, (eql (intern “A”) (intern “A”)), which DOES always return true.

To actually dispatch based on our implementation though, we’ll write a wrapper around our nonportable-function:

(defun my-func-wrapper (value &key (implementation (lisp-implementation-type)))
  (my-nonportable-func value (intern implementation)))

And now, calling #’my-func-wrapper, we see that it calls the generic function specific to our implementation (SBCL):

CL-USER> (my-func-wrapper 'some-value)
=>
"running my-nonportable-func with SBCL specific features"

And if we override the ‘implementation argument’s default value of our actual implementation, with say, cLisp, then we see that the generic function written specifically to use cLisp features is called:

CL-USER> (my-func-wrapper 'some-value :implementation "cLisp")
=>
"running my-nonportable-func using features specific to cLisp"

If in future our code finds its way into the hands of a not-yet-existing implementation of Common Lisp, then the generic func using only standardised features is called:

CL-USER> (my-func-wrapper 'some-value :implementation "future-lisp")
=>
"running my-nonportable-func using only standardised features"

All this is to say that you can, with extra effort, write out several versions of functions that exploit specific features of the lisp-implementation the code is running inside of. Using generic functions lets us dispatch on the implementation. Generic dispatch could be used to dispatch on any number of other factors, such as the user’s keyboard layout, the current datetime, the username of the host machine, the OS, the CPU temperature readings, really just about anything. Generic dispatch can be customised to dispatch not just using type-equality tests, but jury-rigged anything-equality tests.

The more obvious benefit of this, though, is that you can code in an additive style to update your applications for newer standards/operating-systems/etc, while maintaining legacy support since you’re not replacing or removing the old code. Your code can be open for extension while being closed for modification.

Leave a Reply

Your email address will not be published. Required fields are marked *