-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
protocols.mal
95 lines (90 loc) · 3.5 KB
/
protocols.mal
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
;; A sketch of Clojure-like protocols, implemented in Mal
;; By chouser (Chris Houser)
;; Original: https://gist.github.com/Chouser/6081ea66d144d13e56fc
;; This function maps a MAL value to a keyword representing its type.
;; Most applications will override the default with an explicit value
;; for the `:type` key in the metadata.
(def! find-type (fn* [obj]
(cond
(symbol? obj) :mal/symbol
(keyword? obj) :mal/keyword
(atom? obj) :mal/atom
(nil? obj) :mal/nil
(true? obj) :mal/boolean
(false? obj) :mal/boolean
(number? obj) :mal/number
(string? obj) :mal/string
(macro? obj) :mal/macro
true
(let* [metadata (meta obj)
type (if (map? metadata) (get metadata :type))]
(cond
(keyword? type) type
(list? obj) :mal/list
(vector? obj) :mal/vector
(map? obj) :mal/map
(fn? obj) :mal/function
true (throw "unknown MAL value in protocols"))))))
;; A protocol (abstract class, interface..) is represented by a symbol.
;; It describes methods (abstract functions, contracts, signals..).
;; Each method is described by a sequence of two elements.
;; First, a symbol setting the name of the method.
;; Second, a vector setting its formal parameters.
;; The first parameter is required, plays a special role.
;; It is usually named `this` (`self`..).
;; For example,
;; (defprotocol protocol
;; (method1 [this])
;; (method2 [this argument]))
;; can be thought as:
;; (def! method1 (fn* [this]) ..)
;; (def! method2 (fn* [this argument]) ..)
;; (def! protocol ..)
;; The return value is the new protocol.
(defmacro! defprotocol (fn* [proto-name & methods]
;; A protocol is an atom mapping a type extending the protocol to
;; another map from method names as keywords to implementations.
(let* [
drop2 (fn* [args]
(if (= 2 (count args))
()
(cons (first args) (drop2 (rest args)))))
rewrite (fn* [method]
(let* [
name (first method)
args (nth method 1)
argc (count args)
varargs? (if (<= 2 argc) (= '& (nth args (- argc 2))))
dispatch `(get (get @~proto-name
(find-type ~(first args)))
~(keyword (str name)))
body (if varargs?
`(apply ~dispatch ~@(drop2 args) ~(nth args (- argc 1)))
(cons dispatch args))
]
(list 'def! name (list 'fn* args body))))
]
`(do
~@(map rewrite methods)
(def! ~proto-name (atom {}))))))
;; A type (concrete class..) extends (is a subclass of, implements..)
;; a protocol when it provides implementations for the required methods.
;; (extend type protocol {
;; :method1 (fn* [this] ..)
;; :method2 (fn* [this arg1 arg2])})
;; Additionnal protocol/methods pairs are equivalent to successive
;; calls with the same type.
;; The return value is `nil`.
(def! extend (fn* [type proto methods & more]
(do
(swap! proto assoc type methods)
(if (first more)
(apply extend type more)))))
;; An object satisfies a protocol when its type extends the protocol,
;; that is if the required methods can be applied to the object.
(def! satisfies? (fn* [protocol obj]
(contains? @protocol (find-type obj))))
;; If `(satisfies protocol obj)` with the protocol below
;; then `(method1 obj)` and `(method2 obj 1 2)`
;; dispatch to the concrete implementation provided by the exact type.
;; Should the type evolve, the calling code needs not change.