I’ve known about Clojure multimethods of course, but I never really used them much. I didn’t really have data that I’d need polymorphism like multimethods to handle, and when I did need something like that I’d use protocols. However protocols dispatch based on class, so when I faced the problem of handling ActivityPub objects that are all maps, it was time for defmulti
to save the day.
Clojure multimethods allow for arbitrary dispatch functions, so it’s way more flexible than what can be achieved with protocols. If I want to identify my data by some set of keywords, I can use the dispatch function to normalize values to the expected shape (turning strings into namespaced keywords for example).
(defmulti -store-resource
(fn -store-resource-dispatch [object]
(some->> object
(:type)
(str)
(.toLowerCase)
(keyword "kitsune.fed.resource"))))
On this occasion I learned something super cool about multimethods though: while the “obvious” first check when dispatching is equality (eg if the return value of the dispatch function is a known defmethod
), but that’s not all. Turns out it’s possible to organize namespaced keywords into hierarchies that feel really similar to OOP class inheritance. It’s of course not classes, but with this method it’s possible to explicitly define the relationship of various kinds of data.
For example in the case of ActivityPub, there are a bunch of actor types, things that can “do” something. The Person type is the most common, used to represent real human users. However it’s also possible to have bots as Application or Service type. There are also the Group and Organization types that represent a collective of people acting together. In my microblogging use case though I just want to treat all of these things (internally) as “actors” for all the validation and storage. In an OOP language I could make a bunch of classes for each of these types that all inherit from some common Actor class. Using Clojure hierarchies, I can define the relationship between the types similarly.
(derive ::person ::actor) ;; the Person type is an actor
(derive ::application ::actor) ;; the Application type is an actor as well
(derive ::group ::actor) ;; Groups are actors too
Having derived the various types from ::actor
, I then don’t have to add a separate defmethod
for all of them, and can use ::actor
instead.
;; instead of this
(defmethod store-resource ::person
,,,)
(defmethod store-resource ::application
,,,)
(defmethod store-resource ::group
,,,)
;; it can be written concisely as
(defmethod store-resource ::actor
,,,)
In a context like ActivityPub that definitely feels like a very OOP data model, being able to derive
like this means I can have very flexible polymorphism without having 9001 classes (records? types?) that I’d have to maintain and keep consistent.