-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathmulti-methods.el
444 lines (382 loc) · 16.6 KB
/
multi-methods.el
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
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
(require 'defn)
(require 'utils)
(require 'functional)
(require 'cl)
(provide 'multi-methods)
(defvar *hierarchy-weak-table* (make-hash-table :test 'eql :weakness t) "Weak table for keeping track of hierarchies.")
(defvar *multi-method-heirarchy* (alist>> :down nil
:up nil
:resolutions nil) "The default multimethod hierarchy used for isa? dispatch.")
(defun make-hierarchy ()
"Create a hierarchy for multi-method dispatch."
(let ((tag (gensym "hierarchy-tag")))
(puthash tag t *hierarchy-weak-table*)
(alist>> ::::hierarchy-tag tag)))
(defun hierarchyp (object)
"Tests to see if an object is a hierarchy."
(and (listp object)
(let-if tag (alist object ::::hierarchy-tag)
(gethash tag *hierarchy-weak-table* )
nil)))
(defun mk-dispatch-table-name (method)
"generates the symbol for a dispatch table for METHOD"
(internf "--%s-dispatch-table" method))
(defun mk-dispatch-hierarchy-name (method)
"generates the symbol for hierarchy name for METHOD"
(internf "--%s-hierarchy-table" method))
(defun mk-dispatch-function-name (method)
"generates the symbol for the dispatch function for METHOD"
(internf "--%s-dispatcher" method))
(defun mk-default-method-name (method)
"generates the symbol for the default method for METHOD"
(internf "--%s-default-method" method))
(defun make-keyword-accessor (kw)
"Creates an accessor for tables looking for KW"
(lexical-let ((kw kw))
(lambda (table &rest args) (table-like-get table kw))))
(defun clear-dispatch-cache-raw ()
"Clear the dispatch cache for the hierarchy in the dynamic scope."
(alist! *multi-method-heirarchy* ::::dispatch-cache nil)
t)
(defun clear-dispatch-cache (&rest args)
"Retrieve the cache of dispatches for the currently scoped hierarchy, or for one passed in."
(case (length args)
((0) (clear-dispatch-cache-raw))
((1) (let ((*multi-method-heirarchy* (car args)))
(clear-dispatch-cache-raw)))
(otherwise
(error "clear-dispatch-cache: Takes either 0 or 1 arguments."))))
(defun over-all-args (kw/f)
(lexical-let ((kw/f kw/f))
(if (functionp kw/f)
(lambda (&rest args)
(map 'vector kw/f args))
(lambda (&rest args)
(map 'vector
(lambda (tble) (table-like-get tble kw/f))
args)))))
(defun macro-functionp (object)
(cond
((functionp object) t)
((and (listp object)
(= 2 (length object))
(eq (car object) 'function)))
;(functionp (cadr object)))) ;
(t nil)))
(defmacro* defmulti (name dispatch &optional (doc "") (hierarchy-expression '*multi-method-heirarchy*))
"Define a multi-method NAME with dispatch function DISPATCH. DEFUNMULTI defines specific instances of the method."
(let ((table-name (mk-dispatch-table-name name))
(default-method-name (mk-default-method-name name))
(dispatch-name (mk-dispatch-function-name name))
(args-name (gensymf "multi-%s-args" name))
(internal-name (gensymf "multi-%s-holder" name))
(hierarchy-name (mk-dispatch-hierarchy-name name))
(temp (gensym)))
`(progn
(defvar ,default-method-name nil)
(defvar ,hierarchy-name nil ,(format "dispatch hierarchy for %s" name))
(setq ,hierarchy-name ,hierarchy-expression)
(defvar ,table-name (alist>>) ,(format "dispatch-table for %s" name))
(setq ,table-name (alist>>))
(let ((,temp ,dispatch))
(defvar ,dispatch-name ,temp ,(format "dispatch-function for %s" name))
(setq ,dispatch-name ,temp)
(unless (functionp ,dispatch-name)
(print (format "Creating a dispatch function for %S. You may need to define %S before declaring the multimethod if you don't mean to use table-based dispatch." ,dispatch-name ,dispatch-name))
(setq ,dispatch-name (make-keyword-accessor ,dispatch-name))))
(defun ,name (&rest ,args-name)
,doc
(let* ((*multi-method-heirarchy* ,hierarchy-name)
(,internal-name (isa-dispatch-memo (apply ,dispatch-name ,args-name) ,table-name (make-resolve-by-table (alist *preferred-dispatch-table* ',name) ',name ) ,default-method-name)))
(if ,internal-name (apply ,internal-name ,args-name)
(error (format ,(format "No known method for args %%S for multimethod %s.\n Dispatch value is: %%S" name) ,args-name (apply ,dispatch-name ,args-name)))))))))
(defmacro* defunmethod-default (name arglist &body body)
"Define a method of last resort for the method NAME, called when no match is available in the dispatch table."
`(progn
(let ((*multi-method-heirarchy* ,(mk-dispatch-hierarchy-name name)))
(clear-dispatch-cache))
(setq ,(mk-default-method-name name)
(lambda ,arglist
,@body))
',name))
(defmacro* undefunmethod (name value)
"Undefine the method for the multimethod NAME and dispatch value VALUE."
(let ((table-name (mk-dispatch-table-name name)))
`(let ((*multi-method-heirarchy* ,(mk-dispatch-hierarchy-name name)))
(clear-dispatch-cache)
(setq ,table-name
(dissoc-equal ,table-name ,value)))))
(defmacro* defunmethod (name value arglist &body body)
"Define a method using DEFUN syntax for the dispatch value VALUE."
(let ((g (gensym))
(table-name (mk-dispatch-table-name name)))
`(let ((,g (lambda ,arglist ,@body)))
(let ((*multi-method-heirarchy* ,(mk-dispatch-hierarchy-name name)))
(clear-dispatch-cache))
(setq ,table-name
(alist-equal>> ,table-name ,value ,g))
',name)))
(defmacro* defunmethod/alias (name value lambda-yielding-expression)
"Define a method using DEFUN syntax for the dispatch value VALUE."
(let ((g (gensym))
(table-name (mk-dispatch-table-name name)))
`(let ((,g ,lambda-yielding-expression))
(let ((*multi-method-heirarchy* ,(mk-dispatch-hierarchy-name name)))
(clear-dispatch-cache))
(setq ,table-name
(alist-equal>> ,table-name ,value ,g))
',name)))
(defvar *preferred-dispatch-table* nil "Table of method dispatch resolution rules.")
(defun prefer-method-fun (name pref-val not-pref-val)
"Indicate that the NAMEd multimethod should prefer PREF-VAL over NOT-PREF-VAL when dispatching ambiguous inputs."
(let ((subtbl (alist *preferred-dispatch-table* name)))
(alist! subtbl (vector pref-val not-pref-val) pref-val)
(alist! subtbl (vector not-pref-val pref-val) pref-val)
(setf (alist *preferred-dispatch-table* name) subtbl)))
(defmacro prefer-method (name pref-val not-pref-val)
"Declare that a particular dispatch value PREF-VAL is preferred over NOT-PREF-VAL when dispatching the NAMEd method."
`(prefer-method-fun ',name ,pref-val ,not-pref-val))
(defun clear-mm-heirarchy ()
"Clear the hierarchy in the dynamic scope. "
(setq *multi-method-heirarchy* (alist>> :down nil
:up nil
:resolutions nil))
*multi-method-heirarchy*)
(dont-do
(setq *multi-method-heirarchy* (alist>> :down nil
:up nil))
(add-parent-relation :vector :thing)
(add-child-relation :thing :vector))
(defun add-parent-relation (child parent)
"Add a PARENT CHILD relationship to the hierarchy in the dynamic scope."
(let ((parents (alist *multi-method-heirarchy* :up)))
(setf (alist *multi-method-heirarchy* :up) (alist-add-to-set parents child parent)))
*multi-method-heirarchy*)
(defun remove-parent-relation (child parent)
"Add a PARENT CHILD relationship to the hierarchy in the dynamic scope."
(let ((parents (alist *multi-method-heirarchy* :up)))
(setf (alist *multi-method-heirarchy* :up) (alist-remove-from-set parents child parent)))
*multi-method-heirarchy*)
(defun add-child-relation (parent child)
"Add a CHILD PARENT relationship to the hierarchy in the dynamic scope."
(let ((children (alist *multi-method-heirarchy* :down)))
(setf (alist *multi-method-heirarchy* :down) (alist-add-to-set children parent child)))
*multi-method-heirarchy*)
(defun remove-child-relation (parent child)
"Removes a CHILD PARENT relationship to the hierarchy in the dynamic scope."
(let ((children (alist *multi-method-heirarchy* :down)))
(setf (alist *multi-method-heirarchy* :down) (alist-remove-from-set children parent child)))
*multi-method-heirarchy*)
(defun derive2 (parent child)
"Declare a PARENT-CHILD relationship in the dynamically scoped hierarchy."
(clear-dispatch-cache)
(add-child-relation parent child)
(add-parent-relation child parent))
(defun underive2 (parent child)
(clear-dispatch-cache)
(remove-child-relation parent child)
(remove-parent-relation child parent))
(defun derive (&rest args)
"derive H PARENT CHILD establishes a parent-child relationship in H, a heirarchy.
derive PARENT CHILD uses the default hierarchy."
(case (length args)
((2) (apply #'derive2 args))
((3) (let ((*multi-method-heirarchy* (car args)))
(apply #'derive2 (cdr args))))
(t "Derive takes 2 or 3 arguments. More or less were given.")))
(defun underive (&rest args)
"derive H PARENT CHILD establishes a parent-child relationship in H, a heirarchy.
derive PARENT CHILD uses the default hierarchy."
(case (length args)
((2) (apply #'derive2 args))
((3) (let ((*multi-method-heirarchy* (car args)))
(apply #'derive2 (cdr args))))
(t "Derive takes 2 or 3 arguments. More or less were given.")))
(defun* derives-from (child parent &optional (h *multi-method-heirarchy*))
(let ((*multi-method-heirarchy* h))
(derive2 parent child)))
(defun* derive-from (children parent &optional (h *multi-method-heirarchy*))
(let ((*multi-method-heirarchy* h))
(loop for child across (coerce children 'vector) do
(derives-from child parent h))))
(defun mm-parents (child)
"Get the PARENTS of CHILD from the hierachy in the dynamic scope."
(let ((parents (alist *multi-method-heirarchy* :up)))
(alist parents child)))
(defun mm-children (parent)
"Get the CHILDREN of PARENT from the hierachy in the dynamic scope."
(let ((children (alist *multi-method-heirarchy* :down)))
(alist children parent)))
(defun mm-ancestors (child)
"Get all the ancestors of CHILD."
(let* ((parents (mm-parents child))
(ancestors parents)
(done
(if parents nil t)))
(loop while (not done) do
(let ((above (unique (map&filter #'identity #'mm-parents parents) #'equal)))
(if above
(progn
(setq parents above)
(setq ancestors (apply #'append (cons ancestors above))))
(setq done t))))
ancestors))
(defun mm-descendants (child)
"Get all the descendants of CHILD."
(let* ((children (mm-children child))
(descendants children)
(done
(if children nil t)))
(loop while (not done) do
(let ((below (unique (map&filter #'identity #'mm-children children) #'equal)))
(if below
(progn
(setq children below)
(setq descendants (apply #'append (cons descendants below))))
(setq done t))))
descendants))
(defun get-method (name dispatch-value)
"Get the multimethod of kind NAME that is the nearest match for the DISPATCH-VALUE."
(let* ((method-table-name (mk-dispatch-table-name name))
(method-table (eval method-table-name)))
(isa-dispatch dispatch-value method-table (make-resolve-by-table method-table name))))
(defun get-method-funcall (name dispatch-value &rest args)
"Get the method associated with NAME and DISPATCH-VALUE and call it on ARGS."
(let ((m (get-method name dispatch-value)))
(if m (apply m args)
(error "get-method-funcall: No method for %s with dispatch value %S." name dispatch-value))))
(defun get-method-apply (name dispatch-value args)
"Get the method associated with NAME and DISPATCH-VALUE and call it on ARGS, a list."
(let ((m (get-method name dispatch-value)))
(if m (apply m args)
(error "get-method-funcall: No method for %s with dispatch value %S." name dispatch-value))))
; declare some testing hierarchy
(derive :thing :parseable)
(derive :thing :number)
(derive :thing :collection)
(derive :collection :list)
(derive :collection :vector)
(derive :parseable :string)
(derive :parseable :buffer)
(defun isa_ (o1 o2)
"Underlying implementation of isa on regular objects."
(if (equal o1 o2) 0
(let* ((parents (mm-parents o1))
(done (if parents nil t))
(rank (if parents 1 nil)))
(loop while (not done) do
(if (any (mapcar (cr #'equal o2) parents))
(setq done t)
(progn
(setq rank (+ rank 1))
(setq parents
(apply #'append (mapcar #'mm-parents parents)))
(unless parents
(setq done t)
(setq rank nil)))))
rank)))
(defmacro lazy-and2 (e1 e2)
"A lazy and macro."
(let ((e1- (gensym "lazy-and-e1-")))
`(let ((,e1- ,e1))
(if (not ,e1-) nil (and ,e1- ,e2)))))
(defun count-equilength-vectors (list-of)
"Return the number of objects in list-of which are equilength vectors."
(reduce #'+
(let ((n nil))
(mapcar
(lambda (v?)
(if (vectorp v?)
(progn
(if (not n)
(progn
(setq n (length v?))
1)
(if (= n (length v?)) 1 0)))
0))
list-of))))
(defun isa? (o1 o2)
"ISA? test for equality using the default hierarchy. Child ISA? Parent but not vice versa. Isa? returns a number representing the distance to the nearest ancestor that matches. For vectors of objects, these distances are summed. If nil, o1 is not an o2."
(case (count-equilength-vectors (list o1 o2))
((0) (isa_ o1 o2))
((1) nil)
((2) (reduce (lambda (a b)
(cond
((and (numberp a)
(numberp b))
(+ a b))
(t nil)))
(map 'vector #'isa_ o1 o2)))))
(defun resolve-by-first (o r p1 p2)
"Default, dumb conflict resolver."
(list r p1))
(defun make-resolve-by-table (resolution-table method-name)
"Creates a conflict resolution function which checks to see if a method has a specific conflict resolution procedure defined."
(lexical-let ((restbl resolution-table)
(method-name method-name))
(lambda (object rank p1 p2)
(let-if resolution (alist restbl (vector (car p1) (car p2)))
(list rank (alist (list p1 p2) resolution))
(error "Method dispatch ambiguity for %s unresolved (%S vs %S)." method-name (car p1) (car p2))))))
(defun get-dispatch-cache-raw ()
"Get the dispatch cache for the hierarchy in the dynamic scope. Create one if not available."
(let-if cache (alist *multi-method-heirarchy* ::::dispatch-cache)
cache
(let ((cache (make-hash-table :test 'equal)))
(alist! *multi-method-heirarchy* ::::dispatch-cache cache)
cache)))
;; (defun clear-dispatch-cache-raw ()
;; "Clear the dispatch cache for the hierarchy in the dynamic scope."
;; (alist! *multi-method-heirarchy* ::::dispatch-cache nil)
;; t)
;; (defun clear-dispatch-cache (&rest args)
;; "Retrieve the cache of dispatches for the currently scoped hierarchy, or for one passed in."
;; (case (length args)
;; ((0) (clear-dispatch-cache-raw))
;; ((1) (let ((*multi-method-heirarchy* (car args)))
;; (clear-dispatch-cache-raw)))
;; (otherwise
;; (error "clear-dispatch-cache: Takes either 0 or 1 arguments."))))
(defun get-dispatch-cache (&rest args)
"Retrieve the cache of dispatches for the currently scoped hierarchy, or for one passed in."
(case (length args)
((0) (get-dispatch-cache-raw))
((1) (let ((*multi-method-heirarchy* (car args)))
(get-dispatch-cache-raw)))
(otherwise
(error "get-dispatch-cache: Takes either 0 or 1 arguments."))))
(defun* isa-dispatch-memo (object alist resolver &optional (default-method nil))
"Dispatch from an alist table based on ISA? matches. More specific matches are preferred over less, and ambiguous matches will be resolved by the function resolver. (memoized)"
(let* ((cache (get-dispatch-cache))
(method (gethash (list object resolver default-method) cache)))
(if method method
(let ((method (isa-dispatch object alist resolver default-method)))
(puthash (list object resolver default-method) method cache)
method))))
(defun* isa-dispatch (object alist resolver &optional (default-method nil))
"Dispatch from an alist table based on ISA? matches. More specific matches are preferred over less, and ambiguous matches will be resolved by the function resolver."
(let-if method (cadr (cadr (foldl
(lambda (alist-pair best-so-far)
(let ((rank (isa? object (car alist-pair))))
(cond
((not rank) best-so-far)
((not best-so-far) (list rank alist-pair))
((< rank (car best-so-far))
(list rank alist-pair))
((> rank (car best-so-far)) best-so-far)
((= rank (car best-so-far))
(if rank
(funcall resolver object rank alist-pair (cadr best-so-far)) nil)))))
nil
alist)))
method
default-method))
(dont-do
(require 'multi-methods) ;example
(defmulti report :student-name)
(defunmethod report :ricky-gervais (student) "I got an A+")
(defunmethod report :karl-pilkington (student) "Maybe I forgot to sign up for exams.")
(report (alist>> :student-name :ricky-gervais)) ;-> "I got an A+"
(report (alist>> :student-name :karl-pilkington)) ;-> "Maybe I forgot to sign up for exams.")
(report (alist>> :steven-merchant)) ;-> error, no method
)