Polymorphism Beyond Class Hierarchies: defmulti and defmethod
Clojure's multimethods provide open, extensible polymorphism based on an arbitrary dispatch function rather than a single argument's class, as in traditional OO. (defmulti area :shape) declares that calling area will look up the :shape key of its argument, and each (defmethod area :circle [s] ...) registers an implementation for a specific dispatch value; a :default method catches anything unmatched. This lets you dispatch on multiple arguments, on value combinations, or on arbitrary computed criteria, not just a single object's runtime type.
Cricket analogy: An umpire's decision procedure that dispatches on the specific appeal type, LBW versus caught versus run-out, to a different rulebook check for each is exactly a multimethod dispatching on a keyword.
(defmulti area :shape)
(defmethod area :circle [{:keys [radius]}] (* Math/PI radius radius))
(defmethod area :rectangle [{:keys [w h]}] (* w h))
(defmethod area :default [s] (throw (ex-info "Unknown shape" {:shape s})))
(area {:shape :circle :radius 2}) ;=> 12.566370614359172Dispatch Hierarchies with derive
Multimethods can dispatch not just on exact keyword matches but on ad-hoc hierarchies built with derive, which lets you declare that :square derives from ::shape or that ::square derives from ::rectangle; defmethod implementations then match the most specific ancestor available, and prefer-method resolves ambiguity when a value matches two unrelated parents. This gives you inheritance-like reuse without tying your data to a fixed class hierarchy.
Cricket analogy: Classifying a golden duck as a specific kind of duck, which is itself a specific kind of dismissal, lets a stats engine reuse the general dismissal-handling logic while still recognizing the more specific case — exactly like derive's hierarchy.
(derive ::square ::rectangle)
(defmethod area ::rectangle [{:keys [w h]}] (* w h))
(area {:shape ::square :w 4 :h 4}) ;; resolves via the ::rectangle method => 16Protocols: Fast, Type-Based Polymorphism
defprotocol defines a named set of function signatures that types can implement, similar to a Java interface but usable with Clojure's own types, records, and even existing classes; extend-type or extend-protocol wire an implementation onto a specific type after the fact, without needing to modify the original type's source. Protocols dispatch on the runtime type of the first argument alone and compile to efficient JVM interface calls, making them significantly faster than multimethods for the common single-argument, type-based dispatch case — the trade-off is losing multimethods' arbitrary dispatch-function flexibility.
Cricket analogy: A standardized 'how to dismiss' interface that every bowler type, pacer, spinner, or all-rounder, implements in their own specific way, but always exposes the same attempt_dismissal signature, mirrors defprotocol's fixed function set with type-specific implementations.
(defprotocol Shape
(perimeter [this]))
(defrecord Circle [radius]
Shape
(perimeter [this] (* 2 Math/PI radius)))
(defrecord Square [side]
Shape
(perimeter [this] (* 4 side)))
(perimeter (->Circle 3)) ;=> 18.84955592153876Choose protocols when dispatch is purely on the type of the first argument and performance matters; choose multimethods when you need to dispatch on arbitrary logic, multiple arguments, or a value's data rather than its concrete type.
extend-type and extend-protocol let you add protocol implementations to types you don't own, including Java classes, but silently redefining an implementation for a type that already has one will overwrite it without warning — be careful when multiple namespaces extend the same protocol to the same type.
- defmulti declares a dispatch function; defmethod registers an implementation for a specific dispatch value.
- Multimethods can dispatch on arbitrary computed values, multiple arguments, or ad-hoc hierarchies, not just a single argument's type.
- derive builds custom hierarchies so defmethod implementations can match ancestor relationships, with prefer-method resolving ambiguity.
- defprotocol defines a named set of function signatures; extend-type or extend-protocol attach implementations to existing types.
- Protocols dispatch on the runtime type of the first argument and compile to fast JVM interface calls.
- Choose protocols for performance-sensitive, single-argument type-based dispatch; choose multimethods for flexible, arbitrary dispatch logic.
- Both mechanisms provide open, extensible polymorphism without requiring you to modify a type's original definition.
Practice what you learned
1. What does (defmulti area :shape) declare?
2. What is the purpose of a :default defmethod?
3. What does (derive ::square ::rectangle) enable?
4. What do protocols dispatch on?
5. Why might you choose a multimethod over a protocol?
Was this page helpful?
You May Also Like
Higher-Order Functions in Clojure
Learn how Clojure treats functions as first-class values, letting you pass them as arguments, return them from other functions, and compose new behavior with comp, partial, and complement.
Threading Macros
Learn how ->, ->>, some->, some->>, and as-> rewrite deeply nested Clojure expressions into readable, linear pipelines without changing runtime behavior.
Lazy Sequences
Understand how Clojure's lazy sequences defer computation until it's needed, enabling infinite sequences, and learn the chunking and memory pitfalls that trip up beginners.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics