off-topic 2026-03-12

I posted a thing last night (at 2am, so please forgive the grammar). It's a non-obvious pattern in Clojure that I've recently had a use for. It's kind of like inheritance. Sort of. Not really, but… https://dev.to/quoll/clojure-inheritance-sort-of-2i6i

🎉 12

A friend read it today and he thought it could be useful in code that he maintains, so I figured I'd mention it to others

Thank you! Fun to read about approaches people used to solve problems.

Alex Miller (Clojure team) 2026-03-12T18:16:42.426789Z

there are other options here too. one option for "protocol over protocol" is to have the fallback Object extension catch a concrete object (which by definition has no extension installed or you wouldn't get to Object) and dynamically install an extension based on interrogating the concrete type. all subsequent calls for that concrete type will be directly routed. For example, here you could do no protocol extension for Footed and just have it's Object extension check if the this object is Feet2 and if so, extend the concrete type for Footed and call get-feet-2:

(extend-protocol Footed Object
  (get-feet [this] 
    (cond 
      (instance? Feet2 this)
      (extend-protocol Footed (class this)
        (get-feet [_] [:left :right]))
      
      ;; if Feet4, extend differently ..., etc
)))

💡 1
☝️ 1
Alex Miller (Clojure team) 2026-03-12T18:17:32.296779Z

or another option is to just directly manipulate the protocol map (which is all the extend stuff does ultimately) - it's not hard to macro multi-extend stuff that way

Alex Miller (Clojure team) 2026-03-12T18:18:00.344029Z

the dynamic extension approach has a full worked example in Clojure Applied iirc

Alex Miller (Clojure team) 2026-03-12T18:21:22.744569Z

that technique was suggested at the very first conj in response to an audience question at the very first Conj in this talk https://www.youtube.com/watch?v=kQhOlWXXl2I (unfortunately there was no mic so the audio is buried, but I dimly recall it being a conversation between Chouser and Rich in the audience, might be misremembering)

At the time, I was looking for something that was like inheritance (which is why I characterized it that way), and I like the way that it just becomes a protocol name added into a deftype or defrecord. Much of this is just messing around with the protocol definitions, but it's nice to stick with the pre-defined macros. I know that there are various other dispatch mechanisms too.

I was NOT familiar enough with protocols to understand a question like that back in 2010! 🙂

Alex Miller (Clojure team) 2026-03-12T18:22:58.121159Z

yeah, it's Rich at about 32 minutes in the audience that makes this suggestion. I remember this so clearly because it blew my mind

Alex Miller (Clojure team) 2026-03-12T18:24:26.020159Z

oh, I used instance? above which is leaning on the protocol interface, but could also be satisfies?

Alex Miller (Clojure team) 2026-03-12T18:26:02.501199Z

David McNeil put together some helpers for "inheritance" a long while back in https://github.com/david-mcneil/clojure-adapt (blog: https://david-mcneil.com/post/3495351254/clojure-protocol-adapters) which makes this a little nicer

The thing that appeals to me about the approach I wrote about is that it's built with the existing protocol macros, and "wires" everything up without conditionals anywhere. The post scattered things around, but when it's collected together it appears declarative, and (thanks to the extend-protocol) quite terse:

(defprotocol Footed (get-feet [_]))
(defprotocol Feet2)
(defprotocol Feet4)

(extend-protocol Footed
 user.Feet2 (get-feet [_] [:l :r])
 user.Feet4 (get-feet [_] [:fl :fr :bl :br]))

(defrecord Bird [name] Feet2)
(defrecord Cat [name] Feet4)

Alex Miller (Clojure team) 2026-03-12T19:17:52.935529Z

the danger of it is you can easily get into ambiguity when you have a type that implements multiple of these interfaces, and currently what happens is undefined

Alex Miller (Clojure team) 2026-03-12T19:18:13.446439Z

in general, you should primarily be extending to concrete types, not interfaces to avoid that

Alex Miller (Clojure team) 2026-03-12T19:21:48.072659Z

Feet2 and Feet4 implies partitioning where there is often overlapping in many real situations. what happens when you have a CatBird that implements both Feet2 and Feet4? undefined here (and in practice, this varies even on different runs of the same version of Java + Clojure)

When things overlap like this is when you need more careful extension mechanisms. Fortunately, my use case wants records with shared protocol implementations, and don't have any overlap. Honestly, my biggest issue is that it's restricted to Clojure. It can't work for ClojureScript.

Interesting, but I didn't grasp the issue with the initial approach of just picking which feet function to extend each record too? Apart from saving a handful of characters? What was wrong with:

(defrecord bird [name]
  Footed
  (get-feet [] (get-two-feet)))
Is it that you have other places in the code where you want to specialize to all animals of two feet?

> While this is a trivial example, with only a single function on the protocol, the need for this pattern becomes apparent when protocols come with multiple functions. I guess you'll end up with a lot of copy/pasting in cases with multiple functions, and thus more error-prone?

I see, like if 2-feet animals had 4 functions specialized to 2 feet and now you want them all. Make sense.

@gunnar that's what I started with. But it was more like:

(defrecord first-record-type [a b c d e]
 CommonOperations
 (op-1 [this args] (common-op-1 this args))
 (op-2 [this args] (common-op-2 this args))
 (op-3 [this args] (common-op-3 this args))
 (op-4 [this args] (common-op-4 this args))
 (op-5 [this args] (common-op-5 this args))
 (op-6 [this args] (common-op-6 this args))
 (op-7 [this args] (common-op-7 this args))
 (op-8 [this args] (common-op-8 this args))
 (op-9 [this args] (common-op-9 this args))
 (op-10 [this args] (common-op-10 this args)))

(defrecord second-record-type [a b c]
 CommonOperations
 (op-1 [this args] (common-op-1 this args))
 (op-2 [this args] (common-op-2 this args))
 (op-3 [this args] (common-op-3 this args))
 (op-4 [this args] (common-op-4 this args))
 (op-5 [this args] (common-op-5 this args))
 (op-6 [this args] (common-op-6 this args))
 (op-7 [this args] (common-op-7 this args))
 (op-8 [this args] (common-op-8 this args))
 (op-9 [this args] (common-op-9 this args))
 (op-10 [this args] (common-op-10 this args)))
And so on for multiple record types. Using the approach I wrote about, I am able to write the records without these all of these stubs:
(defrecord first-record-type [a b c d e] CommonOperationsA)
(defrecord second-record-type [a b c d e] CommonOperationsA)
The implementations of these op-* functions are then inside the (extend-protocol CommonOperations ...)

👌 1

Also, this particular project has a number of different record types.

I can see that. I think a macro over defrecord or extend could remove the boilerplate but keep the solution same as the above maybe? But I guess your approach is also ok, as long as like Alex says you keep multiple inheritance in check.

Not an issue for my use case. The main problem comes about if I need to do the same thing in ClojureScript

Are the functions tied to the record structure as well? Like they expect certain fields to exist?

Yes. The variations to the functions are to read the appropriate fields for the type of object they're on. All of the records use consistent naming of the fields

(extend-protocol Footed Object ...
heh, I was trying shenanigans from the other angle, experimenting with self-validating records (extend-record to IFn + invoke, and use double-dispatch to self-validate and return or error) https://clojurians.slack.com/archives/C03S1KBA2/p1757067778321479

How about a modified version of https://www.evalapply.org/posts/n-ways-to-fizzbuzz-in-clojure/#oop-buzz? Once again, using multiple double-dispatch, with multiple arity.

(def footed
  {2 [:left :right]
   4 [:front-left :front-right :back-left :back-right]
   6 [:front-left :front-right :middle-left :middle-right :back-left :back-right]})

(defprotocol Footed
  (get-feet [this]
            [this num-feet]))

(defrecord Ape [name])
(defrecord Bird [name])
(defrecord Cat [name])
(defrecord Ant [name])

(extend-protocol Footed
  Object
    (get-feet
      ([this]
        (condp instance? this
           user.Ape (get-feet this 2)
           user.Bird (get-feet this 2)
            (get-feet this 4)
           user.Ant (get-feet this 6)
          :else :none))
      ([_this num-feet]
        (get footed num-feet))))

user=> (get-feet (->Ape "Ape"))
[:left :right]

I feel when I need both methods and fields to be in-sync is a part where I'm not sure of what patterns I like best and where it starts to feel very OO, even more so when adding an inheritance. In your case, if you extend a record to a protocol that needs more fields for example, it's kind of just implicit you have to know what fields to bring into your record.

Oh, IMO this is very OO. When the need for this sort of thing comes up, it's a definite code smell. That doesn't mean there aren't legitimate reasons for wanting in Clojure. It's just going to be infrequent.

For sure, I just always wonder if there's an alternative approach to it. For example I found if you design things as namespaces with functions and each function takes its own input params. Then when you do:

(defrecord Foo [name age]
  Bar
  (baz [this] (bar/baz name age)))
It decouples a bit the structure of the data and it makes it more obvious that if you "inherit" this method you need to also have this data on your record. In your case where you have so many methods it might get annoying, but for simpler cases this feels a bit clearer to me and less OO.

I'm probably missing something - why not employ the same approach that uses? extend with maps.

Everything seems to work just fine. No excessive repetition, plus an ability to override the default impl at the moment of extension.

(defprotocol AniProps
  :extend-via-metadata true
  (get-feet [animal] "Get a sequence of feet")
  (get-wings [animal] "Get a sequence of wings")
  (get-eyes [animal] "Get a sequence of eyes"))

(def default-mammal-impl {:get-feet  (fn [_] [:front-left :front-right
                                              :back-left :back-right])
                          :get-wings (fn [_] [])
                          :get-eyes  (fn [_] [:left :right])})

(def default-haplogyne-impl {:get-feet  (fn [_] [:front-left :front-right
                                                 :second-left :second-right
                                                 :third-left :third-right
                                                 :rear-left :rear-right])
                             :get-wings (fn [_] [])
                             :get-eyes  (fn [_] [:ale-left :ale-right
                                                 :pme-left :pme-right
                                                 :ple-left :ple-right])})

(def default-bird-impl {:get-feet  (fn [_] [:left :right])
                        :get-wings (fn [_] [:left :right])
                        :get-eyes  (fn [_] [:left :right])})

(defrecord Cat [name])
(extend Cat AniProps default-mammal-impl)

(defrecord WoodlouseSpider [name])
(extend WoodlouseSpider AniProps default-haplogyne-impl)

(defrecord BlindCaveSpider [name])
(extend BlindCaveSpider AniProps (assoc default-haplogyne-impl
                                   :get-eyes (fn [_] [])))

(defrecord Parrot [name])
(extend Parrot AniProps default-bird-impl)

(def leon-the-one-legged (with-meta (->Parrot "Leon")
                                    {`get-feet (fn [_] [:right])}))

(get-feet leon-the-one-legged)

I do like this approach. Thanks Eugene