Here's how iso looked in arc3: (def iso (x y)
(or (is x y)
(and (acons x)
(acons y)
(iso (car x) (car y))
(iso (cdr x) (cdr y)))))
A few months ago I extended it to support tables: (def iso (x y)
(or (is x y)
(and (acons x)
(acons y)
(iso (car x) (car y))
(iso (cdr x) (cdr y)))
(and (isa x 'table)
(isa y 'table)
(iso (len:keys x) (len:keys y))
(all
(fn(pair)
(let (k v) pair
(iso y.k v)))
tablist.x))))
But that's hacky. The next time I wanted to add a new type I decided to face arc's hacky lack of primitive support for queues, etc. What I want is to break that body for iso up between a core in defgeneric, and a type-specific body in defmethod that is stored in a vtable. Something like this: (= vtables* (table))
(def iso(x y)
(or (is x y)
(and (is type.x type.y)
(if acons.x
(and (iso car.x car.y)
(iso cdr.x cdr.y))
(aif (vtables*!iso type.x)
(it x y)
(let p (pickles* type.x)
(iso p.x p.y))))))
Now we can extend a generic either with a method in vtables, or with a transformer function in pickles.But how do we divide up that body between what's common across all generic
functions, and what's unique to iso's body? Ah, reordering helps: (def iso(x y)
(aif (vtables*!iso type.x)
(it x y)
(aif (pickles* type.x)
(iso it.x it.y)
(or (is x y)
(and acons.x
acons.y
(iso car.x car.y)
(iso cdr.x cdr.y))))))
Now I can write the iso-specific parts in the lower half as: (defgeneric iso(x y)
(or (is x y)
(and (acons x)
(acons y)
(iso car.x car.y)
(iso cdr.x cdr.y))))
Which is identical to the original iso!Now the common part is at the top. So far it's using iso's args, and
different generics could have different args. Make it independent,
dispatching on just the type of the first arg: (def iso args
(aif (vtables*!iso (type car.args))
(apply it args)
(aif (pickles* (type car.args)
(apply iso (map it args))
(let (x y) args
(or (is x y)
(and acons.x
acons.y
(iso car.x car.y)
(iso cdr.x cdr.y))))))
Now we're ready to implement defgeneric using vtables. (= vtables* (table))
(mac defgeneric(name args . body)
`(def ,name allargs
(aif (aand (vtables* ',name) (it (type car.allargs)))
(apply it allargs)
(aif (pickles* (type car.allargs))
(apply ,name (map it allargs))
(let ,args allargs
,@body)))))
Adding to the vtable for tables: (= vtables*!iso!table
(fn(x y)
(and (isa x 'table)
(isa y 'table)
(is (len keys.x) (len keys.y))
(all
(fn(pair)
(let (k v) pair
(iso y.k v)))
tablist.x))))
The key assumption is that each defmethod can handle args of any type without
raising errors or crashing. Hence the redundant check that x is a table.With defmethod sugar that becomes: (defmethod iso table (x y)
(and (isa x 'table)
(isa y 'table)
(is (len keys.x) (len keys.y))
(all
(fn(pair)
(let (k v) pair
(iso y.k v)))
tablist.x)))
So defmethod translates to: (mac defmethod(name type args . body)
`(= ((vtables* ',name) ',type)
(fn ,args
,@body)))
Let's now try a second example: iso for queues, this time using pickle. Let's assume queues are now tagged as 'queue. (= pickles*!queue
qlist)
Translated: (pickle queue qlist)
So the transform is: (mac pickle(type expr)
`(= (pickles* ',type) ,expr))
Cool!(One bug is that vtables for iso is not initialized anyplace. Exercise for the reader.) --- How does this defgeneric compare with aw's extend (http://awwx.ws/extend0)? Pro: defgeneric scales up to large numbers of extensions because of the hash-table lookup of vtables. extend however must string conditionals linearly, which is less efficient. Con: extend can distinguish arbitrary properties because test is passed in as a parameter rather than assumed to be type. We can fix this if we need to, with a programmable function that generates a (potentially non-type) symbol. |