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}
next-method
Inspired by the Common Lisp Object System (CLOS), Methodical methods can call the next-most-specific method, if the
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 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.
: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 wrap :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
methodsAll applicable :before
methods are invoked before the primary method, in order from most-specific (Object before
String) 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 methodas 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 statment 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
methodsAll applicable :after
methods are invoked after the primary method, in order from least-specific (String before
Object) 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]
Access to the other arguments, rather than just the result, lets your :after
methods accomplish more.
: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 results, as needed. Like primary methods (but unlike
:before
and :after
methods), :around
methods have an implicit next-method
; you'll need to call this to invoke
the next method (the next :around
method, or the :before
/primary method(s) if there are no more :around
methods). :around
methods are invoked from least-specific to most-specific:
(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))
;; -> [:string-before :object-before :default :object-after :string-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.
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.
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 get an effective method that combines all auxiliary
methods and primary methods. By default, this effective method is cached.
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-method multifn String (fn [next-method m]
:string))]
((juxt multifn multifn') {:type String}))
;; -> [:object :string]
Note that when using the programmatic functions, primary and :around
methods are each passed an implicit
next-method
arg as their first arg. The defmethod
macro binds this automatically, but you'll need to handle it
yourself when using these functions.
Every operation available for Clojure multimethods, and quite a few more, are available with programmatic functions like
add-primary-method
.
Clojure's multimethods, while quite powerful, are somewhat limited the ways you can customize their behavior. Here's a quick list of some of the endless things you can do with Methodical multimethods, that are simply impossible with vanilla Clojure mulitmethods:
Dispatch with multiple hierarchies (e.g., one for each arg)
Change the strategy used to cache effective methods (the method that is ultimately invoked for a set of args)
Invoke all applicable primary methods, and return a sequence of their results
Dynamically compute new primary or auxiliary methods with users manually adding them
Support default values for part of a dispatch value, e.g. when dispatching off a pair of classes, support [String String]
, [:default String]
, and [String :default]
Combine multiple multimethods into a single multimethod; when invoked, tries invoking each each in turn until it finds one with a matching method implementation
To enable such advanced functionality, Methodical multimethods are divided into four components, and two that manage them:
The method combination, which defines the way applicable primary and auxiliary methods are combined into a single
effective method. The default method combination, thread-last-method-combination
, binds implicit next-method
args for primary and :around
methods, and implements logic to thread the result of each method into the next.
Method combinations also specify with auxiliary method qualifiers (e.g. :before
or :around
) are allowed, and
how defmethod
macro forms using those qualifiers are expanded (e.g., whether they get an implicit next-method
arg). Method combinations implement the MethodCombination
interface.
The method table store primary and auxiliary methods, and return them when asked. The default implementation,
standard-method-table
, uses simple Clojure immutable maps, but there is nothing stopping you from creating an
implementation that ignores requests to store new methods, or dynamically generates a set of methods to return
based on outside factors. Method tables implement the MethodTable
interface.
The dispatcher decides which dispatch value should be used for a given set of argument arguments, which primary
and auxiliary methods from the method table are applicable for that dispatch value, and the order those methods
should be applied in -- which methods are most-specific, and which are least-specific (e.g. String
is usually
more-specific than Object
.) The default implementation, standard-dispatcher
, mimics the behavior of the Clojure
multimethod, using a dispatch function to determine dispatch values, and a single hierarchy and prefers
map to
determine which methods are applicable; you could easily create your own implementation that uses multiple
hierarchies, or one that uses no hierarchies at all, or one with that optimizes performance. Dispatchers implement
the Dispatcher
interface.
A cache, if present, implements a caching strategy for effective methods, so that need not be recomputed on every
invocation. Caches implement the Cache
interface. Depending on whether you create a multimethod via defmulti
or
with the programmatic functions, the cache is either a watching-cache
, which watches the hierarchy referenced by
the dispatcher (default #'clojure.core/global-hierarchy
), clearing the cache when it changes; or
simple-cache
, a bare-bones cache that manages an atom containing a table of dispatch value -> effective method.
You can easily implement alternative caching strategies, such as TTL or LRU caches, or ones that better optimize
memory and locality (e.g. if dispatch values Object and String both have the same effective method, storing that
method once rather than once per dispatch value.)
The method combination, method table, and dispatcher are managed by an object called the multifn impl, which
implements MultiFnImpl
. If this impl supports caching, it manages a cache as well, albeit indirectly (thru its
implementation of the method effective-method
.) The default implementation is actually a combination of two multifn
impls: cached-multifn-impl
manages a cache and wraps standard-multifn-impl
, which itself retains the other three
components.
Finally, the multifn impl is wrapped in StandardMultiFn
, which implements a variety of interfaces, such as
clojure.lang.IObj
, clojure.lang.Named
, clojure.lang.IFn
, as well as MethodCombination
, MethodTable
,
Dispatcher
, and MultiFnImpl
(by providing proxy access to the components via the multifn impl).
You can use alternative components directly in the defmulti
macro by passing :combo
, :method-table
,
dispatcher
, or :cache
:
(m/defmulti custom-multifn
some-dispatch-fn
:combo (m/thread-first-method-combination))
When constructing multimethods programmatically, you can use standard-multifn-impl
and multifn
to create a multifn with the desired
combination of components:
(m/multifn
(m/standard-multifn-impl
(m/thread-last-method-combination)
(m/standard-dispatcher some-dispatch-fn)
(m/standard-method-table))
nil
(m/simple-cache))
As previously mentioned, Methodical ships with a variety of alternative implementations of these components. The following summarizes all component implementations that currently ship with Methodical:
clojure-method-combination
- mimics behavior of vanilla Clojure multimethods. Disallows auxiliary methods;
primary methods do not get an implicit next-method
arg.
clos-method-combination
- mimics behavior of the CLOS standard method combination. Supports :before
, :after
,
and :around
auxiliary methods. Return values of :before
and :after
methods are ignored. :after
methods are
only called with the result of the primary method. Primary and :around
methods are given an implicit
next-method
argument.
thread-last-method-combination
: the default method combination. Similar to clos-method-combination
, but the
result of :before
methods, the primary method, and :after
methods are threaded thru to the next method as the
last argument. :after
methods
thread-first-method-combination
: Like thread-last-method-combination
, but results are threaded into the next
method as the first arg.
Operator method combinations. The following method combinations are inspired by CLOS operator method combinations; each combination behaves similarly, in that it invokes all applicable primary methods, from most-specific to least-specific (String before Object), combining results with an operator. Thus, the result follows this form:
(operator (primary-method-1 args)
(primary-method-2 args)
(primary-method-3 args)))
Operator method combinations support :around
methods, but not :before
or :after
; primary methods do not
support next-method
(but :around
methods do).
The following operator method combinations ship with Methodical:
do-method-combination
-- executes all primary methods sequentially, as if by do
, returning the result of the
least-specific method. The classic use case for this combination is to implement the equivalent of hooks in
Emacs Lisp -- you could, for example, define a system shutdown multimethod, and various implementations can be
added as needed to add additional shutdown operations:
;; This example uses the `everything-dispatcher`, see below
;;
;; defmulti always expects a dispatch fn, but since it's not used by the everything dispatcher we can pass
;; anything
(m/defmulti ^:private shutdown!
:none
:dispatcher (m/everything-dispatcher)
:combo (m/do-method-combination))
(m/defmethod shutdown! :task-scheduler
[]
(println "Shutting down task scheduler..."))
(m/defmethod shutdown! :web-server
[]
(println "Shutting down web server..."))
(m/prefer-method! #'shutdown! :web-server :task-scheduler)
(m/defmethod shutdown! :around :initiate
[]
(println "Initiating shutdown...")
(next-method))
(shutdown!)
;; -> Initiating shutdown...
;; -> Shutting down web server...
;; -> Shutting down task scheduler...
min-method-combination
-- returns the minimum value returned by all primary methods.
max-method-combination
-- returns the maximum value returned by all primary methods.
+-method-combination
-- returns the sum of all values returned by all primary methods. The classic example use
case is calculating total electricity usage from a variety of sources.
seq-method-combination
-- returns a lazy sequence of all values returned by all primary methods.
concat-method-combination
-- returns a lazy concatenated sequence of all values returned by all primary
methods.
seq-method-combination : map :: concat-method-combination : mapcat
``
and-method-combination
-- invokes all primary methods until one returns a non-truthy value, at which point it
short-circuts.
or-method-combination
-- invokes all primary methods until one returns a truthy value, at which points it
short-circuts and returns that value. You could use this method combination to implement a
chain-of-responsibility pattern.
standard-dispatcher
-- Dispatcher that mimics behavior of vanilla Clojure multimethods. Uses a single hierarchy,
dispatch function, default dispatch value, and map of prefers.
everything-dispatcher
-- The default. Dispatcher that always considers all primary and auxiliary methods to be
matches. Does not calculate dispatch values, but does sort methods from most- to least-specific using a hierarchy
and map of prefers. Particularly useful with the operator method combinations.
standard-method-table
-- The default. A simple method table based on Clojure immutable maps.
clojure-method-table
-- Like standard-method-table
, but disallows auxiliary methods.
simple-cache
-- Default for multimethods constructed programmatically. Simple cache that maintains a map of
dispatch value -> effective method.a
watching-cache
-- Default for multimethods constructed via defmulti
. Wraps another cache (by default,
simple-cache
) and maintains watches one or more Vars (by default, #'clojure.core/global-hierarchy
), clearing
the cache when the watched Var changes. Clears watches when cache is finalized.
standard-multifn-impl
-- Basic impl that manages a method combination, dispatcher, and method table.
cached-multifn-impl
-- wraps another multifn impl and an instance of Cache
to implement caching.
Methodical is built with performance in mind. Although it is written entirely in Clojure, and supports many more features, its performance is similar or better to vanilla Clojure multimethods.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close