Methodical, my Clojure library that ports the “generic function” part of the Common Lisp Object System (CLOS) to Clojure (and makes multimethods way more powerful) just got even more powerful. Methodical now supports dispatch on partial defaults!

What does that even mean?

Take a look at this Clojure code:

(defmulti vanilla-multimethod
  identity)

(defmethod vanilla-multimethod [:x :y]
  [_]
  :x-y)

(defmethod vanilla-multimethod [:default :y]
  [_]
  :y-default)

(defmethod vanilla-multimethod :default
  [_]
  :default)

(vanilla-multimethod [nil :y]) ; -> :default

Vanilla multimethods support exactly one default method, and that’s it. If you want to have a method for any dispatch value where the second value is :y, you are completely out of luck. The best you can do is some hackery like this:

(defmethod vanilla-multimethod :default
  [[x y]]
  (let [default-method   (get-method vanilla-multimethod :default)
        x-default-method (get-method vanilla-multimethod [:default y])
        y-default-method (get-method vanilla-multimethod [x :default])]
    (condp not= default-method
      x-default-method (x-default-method [x y])
      y-default-method (y-default-method [x y])
      :default)))

(vanilla-multimethod [nil :y]) ; -> :y-default

That’s something I’ve actually done in real life, in production!

Why?

There’s some real-world use cases where you’d want partial defaults. When writing Metabase’s query processsing code for JDBC databases, I wanted to write a multimethod for reading columns in a ResultSet that dispatched on both the database and the JDBC type. For example, we have a default method for reading java.sql.Types/TIME_WITH_TIMEZONE columns like:

(defmethod read-column-thunk [:default java.sql.Types/TIME_WITH_TIMEZONE]
  ...)

For some databases, we override this method like:

(defmethod read-column-thunk [:postgres java.sql.Types/TIME_WITH_TIMEZONE]
  ...)

So what happens when we call read-column-thunk with the dispatch value [:mysql java.sql.Types/TIME_WITH_TIMEZONE]? By default, it doesn’t work – it will use the :default method rather than the [:default java.sql.Types/TIME_WITH_TIMEZONE] method as we would like.

Can’t I use hierarchies?

The astute reader will note that you can create a keyword hierarchy and have both :postgres and :mysql derive from a common parent keyword (e.g. :database). That’s certainly true! But creating a hierarchy (or using the global hierarchy with namespaced keywords) just for the sake of a multimethod is annoying, and it still wouldn’t handle cases like [nil java.sql.Types/TIME_WITH_TIMEZONE], where the first part of the dispatch value doesn’t derive from the common parent. It doesn’t work as a true fall-thru for any java.sql.Types/TIME_WITH_TIMEZONE.

Enter Methodical

Methodical now supports partial defaults! Here’s how to do it in Methodical:

(require '[methodical.core :as m])

(m/defmulti my-multimethod
  identity)

(m/defmethod my-multimethod [:x :y]
  [_]
  :x-y)

(m/defmethod my-multimethod [:default :y]
  [_]
  :y-default)

(m/defmethod my-multimethod :default
  [_]
  :default)

(my-multimethod [nil :y]) ; -> :y-default

The new multi-default-dispatcher is the default dispatcher, but with Methodical you can customize method dispatch and have it work any way you’d like!

How Can I Learn More?

I’m giving a talk about Methodical at Clojure/north this year, so stay tuned! As far as I know, Clojure/north is going to be a remote conference due to Coronavirus concerns, so you can attend from anywhere in the world!

👇 Like and subscribe below! 👇