Commit | Line | Data |
---|---|---|
dcdb6c02 NB |
1 | ;; A sketch of Clojure-like protocols, implemented in Mal |
2 | ||
3 | ;; By chouser (Chris Houser) | |
4 | ;; Original: https://gist.github.com/Chouser/6081ea66d144d13e56fc | |
5 | ||
83665b4f NB |
6 | ;; This function maps a MAL value to a keyword representing its type. |
7 | ;; Most applications will override the default with an explicit value | |
8 | ;; for the `:type` key in the metadata. | |
9 | (def! find-type (fn* [obj] | |
dcdb6c02 | 10 | (cond |
dcdb6c02 NB |
11 | (symbol? obj) :mal/symbol |
12 | (keyword? obj) :mal/keyword | |
13 | (atom? obj) :mal/atom | |
83665b4f NB |
14 | (nil? obj) :mal/nil |
15 | (true? obj) :mal/boolean | |
16 | (false? obj) :mal/boolean | |
17 | (number? obj) :mal/number | |
18 | (string? obj) :mal/string | |
19 | (macro? obj) :mal/macro | |
20 | true | |
21 | (let* [metadata (meta obj) | |
22 | type (if (map? metadata) (get metadata :type))] | |
23 | (cond | |
24 | (keyword? type) type | |
25 | (list? obj) :mal/list | |
26 | (vector? obj) :mal/vector | |
27 | (map? obj) :mal/map | |
28 | (fn? obj) :mal/function | |
29 | true (throw "unknown MAL value in protocols")))))) | |
dcdb6c02 | 30 | |
83665b4f NB |
31 | ;; A protocol (abstract class, interface..) is represented by a symbol. |
32 | ;; It describes methods (abstract functions, contracts, signals..). | |
33 | ;; Each method is described by a sequence of two elements. | |
34 | ;; First, a symbol setting the name of the method. | |
35 | ;; Second, a vector setting its formal parameters. | |
36 | ;; The first parameter is required, plays a special role. | |
37 | ;; It is usually named `this` (`self`..). | |
38 | ;; For example, | |
39 | ;; (defprotocol protocol | |
40 | ;; (method1 [this]) | |
41 | ;; (method2 [this argument])) | |
42 | ;; can be thought as: | |
43 | ;; (def! method1 (fn* [this]) ..) | |
44 | ;; (def! method2 (fn* [this argument]) ..) | |
45 | ;; (def! protocol ..) | |
46 | ;; The return value is the new protocol. | |
dcdb6c02 | 47 | (defmacro! defprotocol (fn* [proto-name & methods] |
83665b4f NB |
48 | ;; A protocol is an atom mapping a type extending the protocol to |
49 | ;; another map from method names as keywords to implementations. | |
50 | (let* [ | |
51 | drop2 (fn* [args] | |
52 | (if (= 2 (count args)) | |
53 | () | |
54 | (cons (first args) (drop2 (rest args))))) | |
55 | rewrite (fn* [method] | |
56 | (let* [ | |
57 | name (first method) | |
58 | args (nth method 1) | |
59 | argc (count args) | |
60 | varargs? (if (<= 2 argc) (= '& (nth args (- argc 2)))) | |
61 | dispatch `(get (get @~proto-name | |
62 | (find-type ~(first args))) | |
63 | ~(keyword (str name))) | |
64 | body (if varargs? | |
65 | `(apply ~dispatch ~@(drop2 args) ~(nth args (- argc 1))) | |
66 | (cons dispatch args)) | |
67 | ] | |
68 | (list 'def! name (list 'fn* args body)))) | |
69 | ] | |
70 | `(do | |
71 | ~@(map rewrite methods) | |
72 | (def! ~proto-name (atom {})))))) | |
dcdb6c02 | 73 | |
83665b4f NB |
74 | ;; A type (concrete class..) extends (is a subclass of, implements..) |
75 | ;; a protocol when it provides implementations for the required methods. | |
76 | ;; (extend type protocol { | |
77 | ;; :method1 (fn* [this] ..) | |
78 | ;; :method2 (fn* [this arg1 arg2])}) | |
79 | ;; Additionnal protocol/methods pairs are equivalent to successive | |
80 | ;; calls with the same type. | |
81 | ;; The return value is `nil`. | |
dcdb6c02 NB |
82 | (def! extend (fn* [type proto methods & more] |
83 | (do | |
84 | (swap! proto assoc type methods) | |
85 | (if (first more) | |
86 | (apply extend type more))))) | |
87 | ||
83665b4f NB |
88 | ;; An object satisfies a protocol when its type extends the protocol, |
89 | ;; that is if the required methods can be applied to the object. | |
90 | (def! satisfies? (fn* [protocol obj] | |
91 | (contains? @protocol (find-type obj)))) | |
92 | ;; If `(satisfies protocol obj)` with the protocol below | |
93 | ;; then `(method1 obj)` and `(method2 obj 1 2)` | |
94 | ;; dispatch to the concrete implementation provided by the exact type. | |
95 | ;; Should the type evolve, the calling code needs not change. |