Methodical
Functional and flexible multimethods for Clojure. Nondestructive multimethod construction, CLOS-style aux methods and method combinations, partial-default dispatch, easy next-method invocation, helpful debugging tools, and more.
Install / Use
/learn @camsaul/MethodicalREADME
New: The Clojure/north 2020 talk is up!
<img src="https://img.youtube.com/vi/If3GT8zSHfE/maxresdefault.jpg" width="40%">
Methodical

Methodical is a library that provides drop-in replacements for Clojure multimethods and adds several advanced features.
(require '[methodical.core :as m])
(m/defmulti my-multimethod
:type)
(m/defmethod my-multimethod Object
[m]
(assoc m :object? true))
(my-multimethod {:type Object})
;; -> {:type java.lang.Object, :object? true}
Calling the next-most-specific method with next-method
Inspired by the Common Lisp Object System (CLOS), Methodical methods can call the next-most-specific method, if they
should so desire, by calling next-method:
(m/defmethod my-multimethod String
[m]
(next-method (assoc m :string? true)))
(my-multimethod {:type String})
;; -> {:type java.lang.String, :string? true, :object? true}
This makes it easy to reuse shared parent implementations of methods without having to know the exact dispatch value of the next method. In vanilla Clojure multimethods, you'd have to do something like this:
((get-method my-multimethod Object) (assoc m :string? true))
If you're not sure whether a next-method exists, you can check whether it's nil before calling it.
Methodical exports custom clj-kondo configuration and hooks for defmulti
and defmethod; with the exported configuration it will even tell you if you call next-method with the wrong number
of args:

Auxiliary Methods: :before, :after, and :around
Inspired by the CLOS, Methodical multimethods support both primary methods and auxiliary methods. Primary methods
are the main methods that are invoked for a given dispatch value, such as the implementations for String or Object
in the examples above; they are the same type of method vanilla defmethod supports. Auxiliary methods are additional
methods that are invoked :before, :after, or :around the primary methods:
(m/defmethod my-multimethod :before String
[m]
(assoc m :before? true))
(m/defmethod my-multimethod :around String
[m]
(next-method (assoc m :around? true)))
(my-multimethod {:type String})
;; -> {:type java.lang.String, :around? true, :before? true, :string? true, :object? true}
:before methods
All applicable :before methods are invoked before the primary method, in order from most-specific (String before
Object) to least-specific. Unlike the CLOS, which ignores the results of :before and :after auxiliary methods, by
default Methodical threads the result of each :before method into the next method as its last argument. This better
supports Clojure's functional programming style.
(m/defmulti before-example
(fn [x acc]
(:type x)))
(m/defmethod before-example :before String
[x acc]
(conj acc :string))
(m/defmethod before-example :before Object
[x acc]
(conj acc :object))
(m/defmethod before-example :default
[x acc]
(conj acc :default))
(before-example {:type String} [])
;; -> [:string :object :default]
:before methods unlock a whole new range of solutions that would be tedious with vanilla Clojure multimethods:
suppose you wanted add logging to all invocations of a multimethod. With vanilla multimethods, you'd have to add an
individual log statement to every method! With Methodical, just add a new :default :before method:
(m/defmethod my-multimethod :before :default
[& args]
(log/debugf "my-multimethod called with args: %s" args)
;; return last arg so it is threaded thru for next method
(last args))
:after methods
All applicable :after methods are invoked after the primary method, in order from least-specific (Object before
String) to most-specific. Like :before methods, (by default) the result of the previous method is threaded thru as
the last argument of the next function:
(m/defmulti after-example
(fn [x acc]
(:type x)))
(m/defmethod after-example :after String
[x acc]
(conj acc :string))
(m/defmethod after-example :after Object
[x acc]
(conj acc :object))
(m/defmethod after-example :default
[x acc]
(conj acc :default))
(after-example {:type String} [])
;; -> [:default :object :string]
An example usecase for :after is chess. When looking up legal moves, you
implement how each piece can move, then in :after :default limit it to only
spaces on the board.
:around methods
:around methods are called around all other methods and give you the power to choose how or when to invoke those
methods, and modify any arguments passed to them, or their result, as needed. Like primary methods (but unlike
:before and :after methods), :around methods have an implicit next-method argument; you'll need to call this to invoke
the next method. :around methods are invoked from least-specific to most-specific (Object before String):
(m/defmulti around-example
(fn [x acc]
(:type x)))
(m/defmethod around-example :around String
[x acc]
(as-> acc acc
(conj acc :string-before)
(next-method x acc)
(conj acc :string-after)))
(m/defmethod around-example :around Object
[x acc]
(as-> acc acc
(conj acc :object-before)
(next-method x acc)
(conj acc :object-after)))
(m/defmethod around-example :default
[x acc]
(conj acc :default))
(around-example {:type String} [])
;; -> [:object-before :string-before :default :string-after :object-after]
Around methods give you amazing power: you can decider whether to skip invoking next-method altogether, or even
invoke it more than once; you can acquire resources for the duration of the method invocation with with-open or the
like.
Method combinations are discussed more in detail below.
Defining multiple auxiliary methods for the same dispatch value
Unlike primary methods, you can have multiple auxiliary methods for the same dispatch value. However, adding an
additional duplicate auxiliary method every time you reload a namespace would be annoying, so the defmethod macro
automatically replaces existing auxiliary methods for the same multimethod and dispatch value in the same namespace:
(m/defmulti after-example
(fn [x acc]
(:type x)))
(m/defmethod after-example :after String
[x acc]
(conj acc :string))
;; replaces the aux method above
(m/defmethod after-example :after String
[x acc]
(conj acc :string-2))
(m/defmethod after-example :default
[x acc]
(conj acc :default))
(after-example {:type String} [])
;; -> [:default :string-2]
In most cases, this is what you want, and the least-annoying behavior. If you actually do want to define multiple aux methods of the same type for the same multimethod and dispatch value, you can give each method a unique key:
(m/defmulti after-example
(fn [x acc]
(:type x)))
(m/defmethod after-example :after String "first String :after method"
[x acc]
(conj acc :string))
(m/defmethod after-example :after String "another String :after method"
[x acc]
(conj acc :string-2))
(m/defmethod after-example :default
[x acc]
(conj acc :default))
(after-example {:type String} [])
;; -> [:default :string-2 :string]
You can also use this key to remove specific auxiliary methods.
Getting the "effective method"
The effective method is the method that is ultimately invoked when you invoke a multimethod for a given dispatch
value. With vanilla Clojure multimethods, get-method returns this "effective method" (which is nothing more than a
single function); in Methodical, you can use effective-method to build an effective method that combines all auxiliary
methods and primary methods into a single composed function. By default, this effective method is cached.
Constructing and composing multimethods programmatically
Perhaps one of the biggest limitations of vanilla multimethods is that they can't be passed around and modified
on-the-fly like normal functions or other Clojure datatypes -- they're defined statically by defmulti, and methods
can only be added destructively, by altering the original object. Methodical multimethods are implemented entirely as
immutable Clojure objects (with the exception of caching).
(let [dispatch-fn :type
multifn (-> (m/default-multifn dispatch-fn)
(m/add-primary-method Object (fn [next-method m]
:object)))
multifn' (m/add-primary-met
Related Skills
node-connect
338.7kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
83.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
338.7kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
83.6kCommit, push, and open a PR
