A defclass is a named dyn map with a shape tag, and a generic function
dispatches on it two ways: CLOS's, where the dispatch value is the class
of the first argument, and Clojure's, where a body computes it. They are
one mechanism and not two — a class dispatcher is (class-of arg0) as the
dispatch function, which is what lets a method written for the class
point and one written for the value :point be the same branch.
(defclass point [x y])
(point 3 4) ; the constructor, positional
(class-of p) ; :point, or nil for anything else
(defgeneric area [self] dyn)
(defmethod area point [p] (* (get p :x) (get p :y)))
(defmulti describe [x] dyn (get x :kind))
(defmethod describe :square [s] ...)
(defmethod describe :else [s] ...)
A slot is a key in the instance's own map, so get, put and has-key? are
how one is read and written and no operation was added for any of it.
What the class adds is the tag, and the tag lives in the object's header
rather than in a reserved entry — the queue's note said a reserved key
and this departs from it, because a key would be counted by len, walked
by the renderer and compared by equality, so every instance would answer
a length one larger than its slot count and print a key nobody wrote. A
header field cannot be reached by get or put at all, so no user key can
collide with it. It costs nothing: the map arm of flan_obj's union grows
to the size the view arm already had, and sizeof(flan_obj) is unchanged.
It needs no tracing either — the tag is an interned keyword entry, which
is immortal and is not a collector object.
The tag shows up in exactly three places: class-of answers it, equality
compares it (two instances of one class compare by their slots; an
instance and a plain map with the same entries do not, which is
Clojure's answer for a record beside a map), and both renderers print it
— #point{ :x 1 :y 2}, Clojure's own spelling.
None of the four forms reaches the checker. lib/classes.ml turns the
whole declaration list into ordinary defns at the top of build_program,
the way Shim.expand already turns a declare-c into a declare plus a
defn: a class becomes its constructor, a generic becomes one function
whose body binds the dispatch value and compares it down a chain, and a
method becomes a branch of that chain. It is a pass and not a macro
because a macro sees one form and the generic's body is not decidable
until every method is in hand — a method may be written above its
generic, below it, or arrive at a reload an hour later.
That last case is why the method bodies are inlined rather than lifted.
A generic is exactly one top-level name, so adding a method to a running
program is the ordinary redefinition of one function, through the cell
every call site already goes through. session.ml names the generic
alongside the method's own declaration name for that reason. The cost,
recorded rather than hidden: a method is not separately callable and is
not a frame of its own.
A dispatch that finds no method signals NoMethod, a prelude struct
carrying the generic's name and the dispatch value that missed. A
condition and not a trap, because a miss is something a program can be
written to answer, and handler-case around the call is the shape. Its
value field is dyn, the first condition here with one; the per-type
descriptor an item-2 struct carries is what the collector reaches it by.
No restart is established at the miss, which is BoundsError's decision
taken for BoundsError's reason.
Both backends, identically: the two new runtime entry points are
declared in emit.ml and the x86 backend needs nothing, since a dyn call
is a dyn call there. Deferred and written down in FIX.org: inheritance,
multi-argument dispatch, :before/:after/:around, named-slot
construction, unknown-slot checking, and computed dispatch values.
121 lines
5.1 KiB
Plaintext
121 lines
5.1 KiB
Plaintext
;;;; Classes and generic functions, the dyn side's two dispatch styles.
|
|
;;;;
|
|
;;;; A defclass is a named dyn map with a shape tag. The constructor is the
|
|
;;;; class's own name, positional over the slots; the slots are ordinary map
|
|
;;;; keys, so get and put are how one is read and written and nothing new was
|
|
;;;; needed for either. What the class adds is the tag, which lives in the
|
|
;;;; object's header and not in the entries: (len p) is the slot count, no key
|
|
;;;; a program can write collides with it, and it shows up in exactly three
|
|
;;;; places -- class-of, equality, and the printed form #point{ :x 1 :y 2}.
|
|
;;;;
|
|
;;;; The two dispatch styles are one mechanism. A defgeneric dispatches on the
|
|
;;;; class of its first argument, which is Common Lisp's; a defmulti's body IS
|
|
;;;; the dispatch, which is Clojure's. A class dispatcher is the shape tag of
|
|
;;;; the first argument as the dispatch function, so the second spells the
|
|
;;;; first, and a method is a branch either way.
|
|
|
|
(defclass point [x y])
|
|
(defclass circle [r])
|
|
|
|
;; CLOS's half: dispatch on the class of the first argument. The generic
|
|
;; declares the parameters and the return type once; a method states neither.
|
|
(defgeneric area [self] dyn)
|
|
|
|
(defmethod area point [p] (* (get p :x) (get p :y)))
|
|
;; A method may name its parameter whatever it likes -- the generic's name is
|
|
;; bound to it on the way in.
|
|
(defmethod area circle [c] (* 3 (* (get c :r) (get c :r))))
|
|
|
|
;; Clojure's half: the dispatch is a body, over the same parameter list every
|
|
;; method has, answering the value the methods are keyed by.
|
|
(defmulti describe [thing] dyn (get thing :kind))
|
|
|
|
(defmethod describe :square [s] (get s :side))
|
|
(defmethod describe "round" [s] "a round thing, keyed by a string")
|
|
(defmethod describe 7 [s] "the one keyed by a number")
|
|
;; The method that answers when no other does. It is :else, which is the word
|
|
;; match already uses, and it is the last arm whatever order it is written in.
|
|
(defmethod describe :else [s] "something else")
|
|
|
|
;; A generic with no :else: a miss signals, and the program answers it.
|
|
(defgeneric name-of [self] dyn)
|
|
(defmethod name-of point [p] "a point")
|
|
|
|
(defn main [] i32
|
|
(let [p (point 3 4)
|
|
c (circle 2)]
|
|
;; The instance is a map, and prints as one with its tag in front.
|
|
(println p)
|
|
(println (len p))
|
|
(println (get p :x))
|
|
(put p :x 10)
|
|
(println (get p :x))
|
|
(println (has-key? p :x))
|
|
(println (has-key? p :nothing))
|
|
(println (get p :nothing))
|
|
|
|
;; The shape tag, as a value. Every value can be asked; only an instance
|
|
;; answers with a name.
|
|
(println (class-of p))
|
|
(println (class-of c))
|
|
(println (class-of {:x 3 :y 4}))
|
|
(println (class-of 1))
|
|
(println (class-of nil))
|
|
|
|
;; Equality takes the tag into account: two instances of one class compare
|
|
;; by their slots, an instance and a plain map with the same entries do
|
|
;; not, and two classes with the same slots are two classes.
|
|
(println (= (point 1 2) (point 1 2)))
|
|
(println (= (point 1 2) (point 1 3)))
|
|
(println (= (point 1 2) {:x 1 :y 2}))
|
|
(println (= (circle 2) (circle 2)))
|
|
|
|
;; Class dispatch. Same call site, two classes, two methods.
|
|
(println (area p))
|
|
(println (area c))
|
|
|
|
;; Arbitrary dispatch, over three kinds of dispatch value and the
|
|
;; fallback. The dispatch runs on every call, so the value is whatever
|
|
;; the map holds at the time.
|
|
(println (describe {:kind :square :side 5}))
|
|
(println (describe {:kind "round"}))
|
|
(println (describe {:kind 7}))
|
|
(println (describe {:kind :hexagon}))
|
|
;; An empty map on its own is the zero-field struct literal, so it is
|
|
;; bound first -- the dispatch answers nil for a map with no :kind, and
|
|
;; nil finds no method either.
|
|
(let [empty {:no :kind}]
|
|
(println (describe empty)))
|
|
|
|
;; An instance is an ordinary dyn value: it goes in a vec, keys a map,
|
|
;; and is collected like anything else.
|
|
(let [v [p c]]
|
|
(println (len v))
|
|
(println (class-of (at v 1))))
|
|
|
|
;; The miss. No method and no :else, so the generic signals NoMethod, and
|
|
;; handler-case answers the whole form with a value -- the condition
|
|
;; carries the generic's name and the dispatch value that found nothing.
|
|
(println (name-of p))
|
|
(println
|
|
(handler-case (name-of c)
|
|
[(NoMethod [e] (.generic e))]))
|
|
;; The condition carries the dispatch value that found nothing, which for
|
|
;; a defgeneric over a value that is no instance at all is nil.
|
|
(println
|
|
(handler-case (name-of 42)
|
|
[(NoMethod [e] (.value e))])))
|
|
|
|
;; Instances under the collector: enough of them to pass the 1 MiB floor
|
|
;; many times over, with one live instance in a rooted global. A marker that
|
|
;; lost an instance's slots would free something live and the sum would come
|
|
;; out wrong -- the tag itself is an interned keyword and immortal, which is
|
|
;; why it needs no tracing.
|
|
(let [total (point 0 0)]
|
|
(dotimes [i 50000]
|
|
(let [q (point i "forty-seven bytes of text to fatten each row")]
|
|
(put total :x (+ (get total :x) (len q)))))
|
|
(println (get total :x))
|
|
(println (class-of total)))
|
|
0)
|