/
impl.clj
281 lines (230 loc) · 13.1 KB
/
impl.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
(ns methodical.impl
"Convenience constructors for various implementations of the different component parts of a Methodical multifn."
(:require [methodical.impl.cache
[simple :as cache.simple]
[watching :as cache.watching]]
[methodical.impl.combo
[clojure :as combo.clojure]
[clos :as combo.clos]
[operator :as combo.operator]
[threaded :as combo.threaded]]
[methodical.impl.dispatcher
[everything :as dispatcher.everything]
[multi-default :as dispatcher.multi-default]
[standard :as dispatcher.standard]]
[methodical.impl.method-table
[clojure :as method-table.clojure]
[standard :as method-table.standard]]
[methodical.impl.multifn
[cached :as multifn.cached]
[standard :as multifn.standard]]
[methodical.impl.standard :as impl.standard]
methodical.interface)
(:import methodical.impl.standard.StandardMultiFn
[methodical.interface Cache Dispatcher MethodCombination MethodTable MultiFnImpl]))
;;;; ### Method Combinations
(defn clojure-method-combination
"Simple method combination strategy that mimics the way vanilla Clojure multimethods combine methods; that is, to say,
not at all. Like vanilla Clojure multimethods, this method combination only supports primary methods."
^MethodCombination []
(combo.clojure/->ClojureMethodCombination))
(defn clos-method-combination
"Method combination stategy that mimics the standard method combination in the Common Lisp Object System (CLOS).
Supports `:before`, `:after`, and `:around` auxiliary methods. The values returned by `:before` and `:after` methods
are ignored. Primary methods and around methods get an implicit `next-method` arg (see Methodical dox for more on
what this means)."
^MethodCombination []
(combo.clos/->CLOSStandardMethodCombination))
(defn thread-first-method-combination
"Similar the the standard CLOS-style method combination, but threads the result of each `:before` and `:after`
auxiliary methods, as well as the primary method, as the *first* arg of subsequent method invocations."
^MethodCombination []
(combo.threaded/threading-method-combination :thread-first))
(defn thread-last-method-combination
"Similar the the standard CLOS-style method combination, but threads the result of each `:before` and `:after`
auxiliary methods, as well as the primary method, as the *last* arg of subsequent method invocations."
^MethodCombination []
(combo.threaded/threading-method-combination :thread-last))
;;;; #### CLOS-Inspired Operator Method Combinations
;;; These combinations all work more or less the same way: they invoke *all* applicable primary methods, in order from
;;; most-specific to least specific, reducing the results using the function matching their name, e.g.
;;;
;;; (reduce + (method-1) (method-2) (method-3)) ; `+` method combination
;;;
;;; The following combinations all share the same constraints: they all support `:around` and `:primary` methods, but
;;; not `:before` or `:after` methods. (The only reason this is the case is because that's how it is in CLOS; there's
;;; no reason they *can't* support `:before`, `:after`, `:between`, :`around-each`, :or any other insane auxiliary
;;; method type; these may be added at some point in the future.
;;;
;;; Because all of these combinations automatically invoke *all* relevant primary methods, like CLOS, their primary
;;; methods *do not* get an implicit `next-method` arg; however, `:around` methods still get it (and are still
;;; required to call it.)
(defn do-method-combination
"Based on the CLOS `progn` method combination. Sequentially executes *all* applicable primary methods, presumably for
side-effects, in order from most-specific to least-specific; returns the value returned by the least-specific
method. `do` method combinations support `:around` auxiliary methods, but not `:before` or `:after` methods."
^MethodCombination []
(combo.operator/operator-method-combination :do))
(defn min-method-combination
"Based on the CLOS method combination of the same name. Executes *all* applicable primary methods, returning the
minimum value returned by any implementation. Like `do` method combinations, `min` supports `:around` auxiliary
methods, but not `:before` or `:after`."
^MethodCombination []
(combo.operator/operator-method-combination :min))
(defn max-method-combination
"Executes *all* applicable primary methods, and returns the maximum value returned by any one implemenation. Same
constraints as othe CLOS operator-style method combinations."
^MethodCombination []
(combo.operator/operator-method-combination :max))
(defn +-method-combination
"Executes *all* applicable primary methods, returnings the sum of the values returned by each method. Same constraints
as othe CLOS operator-style method combinations."
^MethodCombination []
(combo.operator/operator-method-combination :+))
(defn seq-method-combination
"Executes *all* applicable primary methods, from most-specific to least-specific; returns a sequence of results from
the method invocations. Inspired by CLOS `nconc` and `append` method combinations, but unlike those, this
combination returns a completely lazy sequence. Like other CLOS-operator-inspired method combinations, this
combination currently supports `:around` methods, but not `:before` or `:after` methods."
^MethodCombination []
(combo.operator/operator-method-combination :seq))
(defn concat-method-combination
"Like the `seq-method-combination`, but concatenates all the results together.
seq-method-combination : map :: concat-method-combination : mapcat"
^MethodCombination []
(combo.operator/operator-method-combination :concat))
(defn and-method-combination
"Invoke *all* applicable primary methods, from most-specific to least-specific; reducing the results as if by `and`.
Like `and`, this method invocation short-circuits if any implementation returns a falsey value. Otherwise, this
method returns the value returned by the last method invoked."
^MethodCombination []
(combo.operator/operator-method-combination :and))
(defn or-method-combination
"Like the `and` combination, but combines result as if by `or`; short-circuits after the first matching primary method
returns a truthy value."
^MethodCombination []
(combo.operator/operator-method-combination :or))
;;;; ### Dispatchers
(defn standard-dispatcher
"Create a stanadrd Methodical multifn dispatcher. The standard dispatcher replicates the way vanilla Clojure
multimethods handle multimethod dispatch, with support for a custom `hierarchy`, `default-value` and map of
`prefers`."
{:style/indent 1}
^Dispatcher [dispatch-fn & {:keys [hierarchy default-value prefers]
:or {hierarchy #'clojure.core/global-hierarchy
default-value :default
prefers {}}}]
{:pre [(ifn? dispatch-fn) (var? hierarchy) (map? prefers)]}
(dispatcher.standard/->StandardDispatcher dispatch-fn hierarchy default-value prefers))
(defn everything-dispatcher
"A Dispatcher that always considers *all* primary and auxiliary methods to be matches; does not calculate dispatch
values for arguments when invoking. Dispatch values are still used to sort methods from most- to least- specific,
using `hierarchy` and map of `prefers`."
^Dispatcher [& {:keys [hierarchy prefers]
:or {hierarchy #'clojure.core/global-hierarchy
prefers {}}}]
(dispatcher.everything/->EverythingDispatcher hierarchy prefers))
(defn multi-default-dispatcher
"Like the standard dispatcher, with one big improvement: when dispatching on multiple values, it supports default
methods that specialize on some args and use the default for others. (e.g. `[String :default]`)"
{:style/indent 1}
^Dispatcher [dispatch-fn & {:keys [hierarchy default-value prefers]
:or {hierarchy #'clojure.core/global-hierarchy
default-value :default
prefers {}}}]
{:pre [(ifn? dispatch-fn) (var? hierarchy) (map? prefers)]}
(dispatcher.multi-default/->MultiDefaultDispatcher dispatch-fn hierarchy default-value prefers))
;;;; ### Method Tables
(defn clojure-method-table
"Create a new Clojure-style method table. Clojure-style method tables only support primary methods."
(^MethodTable []
(clojure-method-table {}))
(^MethodTable [m]
{:pre [(map? m)]}
(method-table.clojure/->ClojureMethodTable m)))
(defn standard-method-table
"Create a new standard method table that supports both primary and auxiliary methods."
(^MethodTable []
(standard-method-table {} {}))
(^MethodTable [primary aux]
{:pre [(map? primary) (map? aux)]}
(method-table.standard/->StandardMethodTable primary aux)))
;;; ### Caches
(defn simple-cache
"Create a basic dumb cache. The simple cache stores"
(^Cache []
(simple-cache {}))
(^Cache [m]
(cache.simple/->SimpleCache (atom m))))
(defn watching-cache
"Wrap `cache` in a `WatchingCache`, which clears the cache whenever one of the watched `references` (such as vars or
atoms) changes. Intended primarily for use with 'permanent' MultiFns, such as those created with `defmulti`; this is
rarely needed or wanted for transient multifns."
^Cache [cache references]
(cache.watching/add-watches cache references))
;;; ### MultiFn Impls
(defn standard-multifn-impl
"Create a basic multifn impl using method combination `combo`, dispatcher `dispatcher`, and `method-table`."
^MultiFnImpl [combo dispatcher method-table]
{:pre [(instance? MethodCombination combo)
(instance? Dispatcher dispatcher)
(instance? MethodTable method-table)]}
(multifn.standard/->StandardMultiFnImpl combo dispatcher method-table))
(defn default-multifn-impl
"Create a basic multifn impl using default choices for method combination, dispatcher, and method table."
{:arglists '([dispatch-fn & {:keys [hierarchy default-value prefers]}])}
^MultiFnImpl [dispatch-fn & dispatcher-options]
(standard-multifn-impl
(thread-last-method-combination)
(apply multi-default-dispatcher dispatch-fn dispatcher-options)
(standard-method-table)))
(defn clojure-multifn-impl
"Create a mulitfn impl that largely behaves the same way as a vanilla Clojure multimethod."
{:arglists '([dispatch-fn & {:keys [hierarchy default-value prefers method-table]}])}
^MultiFnImpl [dispatch-fn & {:keys [method-table], :or {method-table {}}, :as options}]
(let [dispatcher-options (apply concat (select-keys options [:hierarchy :default-value :prefers]))]
(standard-multifn-impl
(clojure-method-combination)
(apply standard-dispatcher dispatch-fn dispatcher-options)
(clojure-method-table method-table))))
(defn clos-multifn-impl
"Convenience for creating a new multifn instances that for the most part mimics the behavior of CLOS generic functions
using the standard method combination. Supports `:before`, `:after`, and `:around` auxiliary methods, but values of
`:before` and `:after` methods are ignored, rather than threaded. Primary and `:around` methods each get an implicit
`next-method` arg."
{:arglists '([dispatch-fn & {:keys [hierarchy default-value prefers primary-method-table aux-method-table]}])}
^MultiFnImpl [dispatch-fn & {:keys [primary-method-table aux-method-table],
:or {primary-method-table {}, aux-method-table {}}
:as options}]
(let [dispatcher-options (apply concat (select-keys options [:hierarchy :default-value :prefers]))]
(standard-multifn-impl
(clos-method-combination)
(apply standard-dispatcher dispatch-fn dispatcher-options)
(standard-method-table primary-method-table aux-method-table))))
(defn cached-multifn-impl
"Wrap a `MultiFnImpl` in a `CachedMultiFnImpl`, which adds caching to calculated effective methods. The cache itself
is swappable with other caches that implement different strategies."
(^MultiFnImpl [impl]
(cached-multifn-impl impl (simple-cache)))
(^MultiFnImpl [impl cache]
(multifn.cached/->CachedMultiFnImpl impl cache)))
;;; Standard MultiFn
(defn uncached-multifn
"Create a new Methodical multifn using `impl` as the multifn implementation; `impl` itself should implement
`MultiFnImpl`. DOES NOT CACHE EFFECTIVE METHODS -- use `multifn` instead, unless you like slow dispatch times."
(^StandardMultiFn [impl]
(uncached-multifn impl nil))
(^StandardMultiFn [impl mta]
(impl.standard/->StandardMultiFn impl mta)))
(defn multifn
"Create a new *cached* Methodical multifn using `impl` as the multifn implementation."
(^StandardMultiFn [impl]
(multifn impl nil))
(^StandardMultiFn [impl mta]
(multifn impl mta (simple-cache)))
(^StandardMultiFn [impl mta cache]
(uncached-multifn (cached-multifn-impl impl cache) mta)))
(def ^{:arglists (:arglists (meta #'default-multifn-impl))}
default-multifn
"Create a new Methodical multifn using the default impl."
(comp multifn default-multifn-impl))