Clojure Exploring Superclasses & Interfaces Lineage

Ever found yourself deep in Clojure code, needing to implement a new type with deftype or defrecord, and suddenly your mind goes blank? “Wait, what interfaces does PersistentVector actually implement? Which protocols should _my_ type satisfy to behave similarly?”

I faced this exact same issue when I wanted to implement a specific type of queue in Clojure.

The Standard Approach clojure.core/supers

If I want to find all superclasses & interfaces of clojure.lang.PersistentVector or [] then I would user supers like so:

(supers (type []))
;; => #{clojure.lang.Reversible clojure.lang.APersistentVector
;;     java.util.SequencedCollection clojure.lang.IMeta
;;     clojure.lang.IPersistentCollection clojure.lang.IObj
;;     clojure.lang.ILookup java.lang.Comparable clojure.lang.Indexed
;;     java.util.RandomAccess clojure.lang.Sequential
;;     clojure.lang.IReduce java.io.Serializable clojure.lang.AFn
;;     clojure.lang.IReduceInit java.lang.Iterable clojure.lang.Counted
;;     java.util.Collection java.util.List clojure.lang.Associative
;;     clojure.lang.IPersistentStack clojure.lang.IFn
;;     java.util.concurrent.Callable clojure.lang.Seqable
;;     clojure.lang.IPersistentVector clojure.lang.IDrop java.lang.Object
;;     clojure.lang.IHashEq clojure.lang.IKVReduce java.lang.Runnable
;;     clojure.lang.IEditableCollection}

This gives a set, but it doesn’t reveal the structure.

We can do better.

Visualising the lineage

I needed a way to see the inheritance and implementation hierarchy more clearly. When implementing my own types, I often want to know the _direct_ interfaces and then explore _their_ supers if needed.

So I wrote this small recursive function:

(defn supers-lineage [class]
  (reduce (fn [acc kls]
            (assoc acc kls (supers-lineage kls)))
          {}
          (bases class)))

This function takes a class, gets its direct bases, and for each base, recursively calls itself, associating the base class/interface with its own nested hierarchy. bases internally calls .getSuperclass and .getInterfaces and concats them.

Seeing the difference

Let’s try this on our vector type again:

(supers-lineage (type []))

Outputs

{clojure.lang.APersistentVector ; Base Abstract Class
 {clojure.lang.AFn               ; Implements AFn
  {java.lang.Object {},          ;  - AFn extends Object
   clojure.lang.IFn             ;  - AFn implements IFn
   {java.util.concurrent.Callable {}, ;   - IFn extends Callable
    java.lang.Runnable {}}},          ;   - IFn extends Runnable
  clojure.lang.IPersistentVector   ; Implements IPersistentVector
  {clojure.lang.Associative       ;  - IPV extends Associative
   {clojure.lang.IPersistentCollection {clojure.lang.Seqable {}}, ; - Assoc extends IPColl (which extends Seqable)
    clojure.lang.ILookup {}},       ; - Assoc extends ILookup
   clojure.lang.Sequential {},      ;  - IPV extends Sequential
   clojure.lang.IPersistentStack   ;  - IPV extends IPStack
   {clojure.lang.IPersistentCollection {clojure.lang.Seqable {}}}, ; - IPStack extends IPColl (which extends Seqable)
   clojure.lang.Reversible {},      ;  - IPV extends Reversible
   clojure.lang.Indexed           ;  - IPV extends Indexed
   {clojure.lang.Counted {}}},      ;   - Indexed extends Counted
  java.lang.Iterable {},          ; Implements Iterable
  java.util.List                 ; Implements List
  {java.util.SequencedCollection  ;  - List extends SequencedCollection
   {java.util.Collection {java.lang.Iterable {}}}}, ; - SeqColl extends Collection (which extends Iterable)
  java.util.RandomAccess {},     ; Implements RandomAccess
  java.lang.Comparable {},        ; Implements Comparable
  java.io.Serializable {},        ; Implements Serializable
  clojure.lang.IHashEq {}},       ; Implements IHashEq
 clojure.lang.IObj               ; Implements IObj
 {clojure.lang.IMeta {}},          ;  - IObj extends IMeta
 clojure.lang.IEditableCollection {}, ; Implements IEditableCollection
 clojure.lang.IReduce              ; Implements IReduce
 {clojure.lang.IReduceInit {}},     ;  - IReduce extends IReduceInit
 clojure.lang.IKVReduce {},       ; Implements IKVReduce
 clojure.lang.IDrop {}}            ; Implements IDrop

Look at that! Now we can clearly trace the lineage. We see that clojure.lang.APersistentVector directly implements clojure.lang.IPersistentVector, java.util.List, clojure.lang.IObj, etc. Furthermore, we can see that IPersistentVector itself extends Associative, Sequential, Indexed, and others. And we can even see that Indexed extends Counted. This nested structure makes the relationships explicit.

I’m thinking of creating a small cljs tool to visualise this as a tree!