CLOS¶
Tiny CLOS is a tiny, er, CLOS. So what is CLOS?
CLOS refers to the Common Lisp Object System. That link will take you through to documents describing meta-object-protocol as well as ’s STklos, ’s Swindle and others all of which give some background and inspiration.
CLOS QuickRef¶
That still doesn’t tell us what CLOS is. The primary difference with more familiar object systems are probably that whilst the class hierarchy is much the same, in that classes have super-classes and slots (or fields), the functions that operate on classes are not elements of the classes but are, so-called, generic functions which will can be applied to any set of arguments.
You then define methods, tied to the generic function by name, which declare that for given parameter classes, use this method. The magic in the system is how to determine which method should be called given a particular set of arguments.
Generics are making their way into other object systems but another key difference, and partly why generic functions are not elements of the class hierarchy, is that they are not restricted to selecting an implementation method based on one (usually the first) of their arguments but use all arguments to select the most specific method.
We now get another intriguing concept. In single-inheritance object
systems you have the concept of being able to call your super-class’
comparable method, often through a function called super
. In
multiple-inheritance object systems that doesn’t make sense as the
function isn’t tied to any one of its arguments and instead you have
the concept of the next most specific method, indeed a chain of them
from the most specific applicable method through to the least specific
applicable method.
That also brings front and centre applicability as the generic function isn’t restricted to having methods appropriate to you and your classes but for any and all classes, should anyone write a method for them.
Here, think of something like a printer. You might have a
print-object
generic function where you can add methods
appropriate for your classes or allow the fallback method (associated
with the base <object>
class) to print nothing or whatever.
CLOS goes a bit further with some concepts to do with methods. In addition to what are called primary methods it has the idea of methods that should be run before or after any corresponding primary methods and even methods that should be run around other methods (whatever that means).
A final word on the meta-object-protocol which is a mechanism by which the behaviour of the object system can be controlled by giving every instance in the system a meta-class. Meta-classes are, of course, just regular classes and all classes are instances of other classes giving the whole enterprise the sense of a digital Gordian knot. When talks about “initializing the braid,” he means it.
Tiny CLOS¶
The Art of the Metaobject Protocol, [GKB91], in 1991, published TinyCLOS in 1992 which has become a popular base for Scheme-ish languages’ object systems.
, after co-authoringMuch of what was described for CLOS is available in Tiny CLOS and where Tiny CLOS is pedagogical in nature, others have looked at making the implementation a little more efficient and broader in scope.
I’ll try to break Tiny CLOS down, throwing in a few notes. Some terms get overused, mind, for example, instance.
One thing to remember, though, is that Tiny CLOS is tiny. A full multi-methods object system in, depending on what support code you include, 600 lines of code. It’ll take far more lines to describe what’s going on.
You’ll want to follow along with tiny-clos.scm
, probably from
the GitHub mirror at https://github.com/kstephens/tinyclos as modern browsers don’t
like FTP links any more.
Memory Model¶
The underlying memory model in Tiny CLOS is a Scheme vector, akin to an Idio array. Anything could have been used, we just want to access individual elements.
The first three elements represent the meta-class, the instance proc and a lock. The lock is used to prevent the instance proc being used for non-generic functions – something we can do in other ways.
The instance proc itself is a slightly disjoint concept. The idea is that if this (instance) value is found to be in functional position, about to be applied to zero or more arguments, then we want something to run to implement behaviour. A vector (array) in functional position isn’t going to do much so we want something to substitute in, in its place, to actually do some work.
All instances will have an instance proc – that’s just the way the data structure is laid out – but it is only a useful concept for generic functions, which are the only things we expect to find in functional position (and therefore need the instance proc to implement behaviour).
The remaining elements are either descriptive (for a class) or data (for instances of classes).
Tiny CLOS goes a step further and instead of returning an instance, the vector, perhaps, returns a function which, when invoked, actually calls the instance proc. This is done for two reasons:
it’s a neat trick meaning that the instance, here, generic function, despite notionally being a data structure, is implicitly invokable
We don’t need to go there as we have a C function,
idio_vm_invoke()
, which decides if something is invokable. We’ve bodged all sorts in there already, symbols and strings will be looked for down thePATH
, continuations are invokable (or rather, we rewrite the stack and jump to a new instruction!) as well as closures and primitives.If we want to add generic functions as something that is invokable, by substituting in the instance proc and carrying on, then so be it.
I first saw this invokable-object trick in Exploring Programming Language Architecture in Perl ([Hai10]) and notes that for Swindle, PLT Scheme “has applicable struct objects”, so, not without precedent.
’because classes and instances are self-referential, it avoids some complications in printing such a value
Instead, the regular printer can simply say this “instance” is an (opaque) function.
The system still needs to get the associated vector so the function is
used as an index into an association list of (function
vector)
tuples which itself is a private variable for a suite of
functions that need access to it.
A class has slots which describe the elements of instances of the class:
direct super-classes
direct slots
the class precedence list (CPL), which is the derived set of all relevant classes drawn from the class and its super-classes
The CPL is critical to getting a deterministic method resolution order (MRO) which, let’s be fair, sounds like a good thing.
the slots, now including the non-duplicated set of super-class slots as well the direct slots
a set of per-slot “getters-n-setters” – often functions that simply access the corresponding slot in the underlying vector
An instance of a class has as many slots as the class description says there should be!
Allocation¶
Noting that we are saying in advance that the object system is going to be self-referential, then at the very base level we need to separate the allocation of an instance from the assignment of elements in it.
Once we’re beyond the bootstrap we can write, with increasing degrees
of “finality”, a make-instance
function that combines allocation
and assignment.
make-instance
is actually make
in Tiny CLOS and other
variants which is fine for them but clashes rather spectacularly with
make for us Shell People.
Class Bootstrap¶
Here we allocate <class>
and, as the very first thing, set its own
meta-class (“instance-class” in the code) to… <class>
, itself.
What we’re saying here is that when we come to do stuff if we want
to ask questions about how to implement the behaviour of class
<class>
then we should query the meta-class, <class>
.
OK, that’s probably not helpful right now so we’ll just leave it
hanging. Of interest, almost everything has a meta-class of
<class>
as, perhaps unsurprisingly, almost everything is an
instance of a class which is described by <class>
.
OK, I’ll stop.
Let’s fill in the rest of <class>
’s slots, as much as we can right
now:
direct-supers
is#n
– to be modified in a momentdirect-slots
is this list of slots names that we’re iterating through right nowcpl
is#n
– to be modified in a momentslots
is the same asdirect-slots
nfields
is the count of the number of slots (seven!)field-initializers
list of nfields functions which set the slot to#n
getters-n-setters
is a list of nfields tuples with an appropriatevector-ref
(%instance-ref
) getter and avector-set!
(%instance-set!
) setter
Of interest, most implementations drop field-initializers
, often
extending getters-n-setters
to include an “init-function” and also
add another slot, name
, for self-reflection purposes.
make-instance I¶
At this point we can create our first make-instance
(make
in
Tiny CLOS, of course) which is only aware of some simple
meta-classes which it knows how to manually construct.
We have the advantage that, at this stage, the computation of the CPL and slots is going to be the straightforward merge of super-classes and super-class direct slots.
We’ll mostly be calling make-instance
as:
make-instance <class> 'direct-supers supers 'direct-slots slots
and can figure out the CPL, slots and compute some getters and setters based on that information.
<top> and <object>¶
We can now create a couple more classes to fix the top of the class tree.
A class, <top>
, with no super-classes or slots and a meta-class of
<class>
(of course). It’s the top of the class hierarchy tree.
A class, <object>
, with a super-class of <top>
, no slots and a
meta-class of <class>
. <object>
will be the base class for
all object system object values. That doesn’t mean it is the base
class for all objects as you might describe native types (pairs,
strings, etc.) slightly differently and, in an object class hierarchy,
directly descended from <top>
. Native types aren’t object system
objects per se but can be manipulated within the guise of an object
system.
Semantic nuance aside, that allows us to patch up <class>
such
that its direct super is <object>
and its CPL is, therefore,
(<class> <object> <top>)
.
We now have a vaguely sensible class hierarchy of <top>
to
<object>
to <class>
all of which have <class>
as a
meta-class.
Base Classes¶
Finally we can create the remaining base classes:
<procedure-class>
the base class of all invokable classesIt has a super-class (and meta-class) of
<class>
and no direct slots.<entity-class>
a feature of MOPsIt has a super-class of
<procedure-class>
and meta-class of<class>
and no direct slots.I’ll be honest, I’ve not quite got a finger on what an entity-class is in this (or any other) context. However, it appears in…
<generic>
the base class of generic functionsIt has a super-class of
<object>
and a meta-class of<entity-class>
.<generic>
does have a direct slot:methods
which is a list of the methods associated with this generic function
Other implementations extend the slots with:
name
documentation
<generic>
is the only standard class that has a meta-class that isn’t<class>
(albeit noting that<entity-class>
has a super(-super)-class of<class>
and therefore you end up with the same slots anyway).<method>
which is the base class of methods for generic functionsIt has a super-class of
<object>
and a meta class of<class>
.<method>
has a couple of direct slots:specializers
which are the classes of the arguments for which this method is appropriateprocedure
which is the actual function to implement the method behaviour
Other implementations extend the slots with:
generic-function
to allow some cross-referencing
All of the above is a little dry, particularly with respect to generic
functions and methods. Getting ahead of ourselves very slightly, in a
moment we’ll be adding a bunch of generic functions and methods, one
of which is initialize
which expects to be passed an instance of
some kind and some initargs. We want to perform different actions
based on the kind of instance passed.
The Tiny CLOS mechanism is a little bit exposed:
add-method generic-function-name method
will addmethod
to the named generic functionmake-method specializers function
creates a method with the given parameter specializers and the given function definition
There’s a couple of slightly confusing aspects here in that there
appear to be two extra arguments supplied to the method’s function
definition: call-next-method
and initargs
, neither of which
appear in the list of specializers.
call-next-method
is a thunk we can call to do the moral equivalent of callingsuper
. It’s always provided to methods so there’s no need to have it in the specializers.initargs
seems, to me, to be anomalous. I think it should appear as a specializer where, as we don’t know what it is and therefore can’t give it a specializer, it will automatically be given<top>
as a specializer. I’ve tacked those on as comments.
(define initialize (make-generic))
(add-method initialize
(make-method (list <object>) ; (list <object> <top>) ??
(lambda (call-next-method object initargs)
...
manipulate the <object> in object
..
)))
(add-method initialize
(make-method (list <class>) ; (list <class> <top>) ??
(lambda (call-next-method class initargs)
...
manipulate the <class> in class
...)))
(add-method initialize
(make-method (list <generic>) ; (list <generic> <top>) ??
(lambda (call-next-method generic initargs)
...
manipulate the <generic> in generic
...)))
I find the Scheme above harder to read than I feel it should be – and I’ve cut out the actual behaviour code! It all looks a bit by rote and indeed STklos leads us to our preferred style through the template, define-method, which looks like a regular function declaration except with the formal parameters optionally qualified by a class:
define-method (initialize (obj <object>) initargs) {
...
manipulate the <object> in obj
...
}
define-method (initialize (cl <class>) initargs) {
...
manipulate the <class> in cl
...
}
define-method (initialize (gf <generic>) initargs) {
...
manipulate the <generic> in gf
...
}
Even better if define-method
can implicitly create the underlying
named generic method.
make-generic¶
We’re not out of the woods yet. There are nine generic functions which define the MOP and, as we can’t casually add generic functions without the MOP, we need to pre-declare these nine. Hence the:
(define initialize (make-generic))
...
which, more or less, reduces to a simple allocation of the generic function.
add-method¶
add-method
wants to do two things:
add the supplied method to the list of methods in the generic function
Of course we need to be careful to remove any existing method with the same specializers as the one we’re adding.
Note, though, that the list of methods is just a list of methods without any ordering. We can’t know what ordering to use until someone invokes the generic function with some particular arguments.
set the generic function’s instance proc, the thing that is going to figure out what the ordered set of applicable methods are with whatever the arguments are at the time of calling
Here, we set the instance proc to the results of a call to the generic function
compute-apply-generic
.Wait, what? Generic functions don’t work yet!
Also note that
compute-apply-generic
is expected to return a function, to become the instance proc, that will accept any number of arguments. We don’t know how many arguments generic functions are going to take in general, so these MOP generic functions must be ready.
compute-apply-generic¶
So, let’s review. add-method
is going to set the instance proc of
some generic function to be the result of calling the generic function
compute-apply-generic
. The act of invoking a generic function is
to actually call the instance proc of the generic function instead.
So, compute-apply-generic
needs an instance proc, right? That
should sort it.
Here, as
notes, we need a couple of carefully crafted functions.we need a bootstrap function because
add-method
is going to call something, right?Here, we explicitly set the instance proc of
compute-apply-generic
to be a one-shot function that simply calls the first of the generic function’s methods, because, well, because that’ll work.Technically, of course, we don’t call the method per se but, rather, apply the function in the method’s
procedure
slot to the supplied arguments.Of interest, as we don’t know (read: can’t figure out) what the next methods should be for this one-off – as we called it blindly without looking at the specializers – we simply pass
#f
forcall-next-method
and trust that, whatever this method is, it won’t call it.we need to
add-method
a genuine method tocompute-apply-generic
whichcatches calls to a magic four generic methods (including
compute-apply-generic
) whereon we call the last defined method – in effect the first method that was added as new methods are pushed on the front of the list of methodsotherwise goes full-on MOP
Here, some of the magic starts taking place.
add-method
adds the method to the generic functions list of methods before trying to set the instance proc to the result of the call tocompute-apply-generic
Now that
add-method
is callingcompute-apply-generic
we run the one-shot function which (blindly) calls the first method on the list of methods (which we just added a moment ago).Here, there’s a sleight of hand as the result of calling
compute-apply-generic
is to return a function, to become the next instance proc. In other words, we can reduce the visual complexity down to:(add-method compute-apply-generic (make-method (list <generic>) (lambda (call-next-method generic) (lambda args ... do something *next time* ...))))
That’s an important distinction, the one-shot function effectively called
(lambda (call-next-method generic) ...)
which returned a new instance proc for next time, not this time.
I suppose there’s a chance that someone “living on the edge” might
want to add their own method to compute-apply-generic
or its
three friends so that the last/first trick means we will always run
the code we expect.
Unless that user was really out there and messed with the list of methods… There’s some commentary about problems with shared mutable state down below.
As it happens, that’s it for compute-apply-generic
, we don’t (need
to) touch it again. It has a special clause for itself (never used)
and its three friends (which we’re about to see) but otherwise will go
full-on MOP for anything else.
The MOP¶
The MOP is actually the whole business of the introspection and
intercession (fiddling with!) of instances and classes but the real
magic lies in the full-on MOP part elegantly realised in
compute-apply-generic
as:
((compute-apply-methods generic) ((compute-methods generic) args) args)
compute-apply-methods
, compute-methods
and the unseen
compute-method-more-specific?
are the other three special generic
functions which, like compute-apply-generic
only have a single
method created for them and are handled separately.
They also return functions hence their results are applied in turn.
Let’s try to figure this out.
In the first instance, and this will be true for all four generic
functions, they were called with an argument, generic
, which is in
scope of the function they returned.
In the case of compute-apply-generic
, that function became the
instance proc and has now been called with some arbitrary args
.
So, both generic
and args
are available to us.
compute-methods
returns a function which, when applied to some
arguments, will figure out an ordered list of the applicable methods.
In other words, it will ignore methods whose specializers are not
appropriate for these arguments and then sort the resultant list by
some reasoning.
The applicability of a method is determined by ensuring that each specializer in the method’s specializers is a member of the CPL of the class of the corresponding argument.
Sorting the resultant list is quite involved. gsort
in
Tiny CLOS is a simple wrapper to the slightly different
calling conventions of sort
across the various Scheme
implementations. We pass gsort
a function that takes two methods
as parameters.
The sorting function, in turn, calls the generic function
compute-method-more-specific?
which itself returns a function that
takes the two method parameters and args
again.
Here, now, we can walk over the specializers of each of the methods and the corresponding argument asking if the specializer of one method is a member of the list returned by asking for the membership of the other method’s specializer in the CPL of the class of the argument.
That probably needs an example but is a neat side-effect of the way memq works in that it doesn’t simply return true or false about membership but returns the rest of the list starting at the match which can be exploited to see if the one is a (in this case) “more specific” element than the other:
Idio> cpl := '(C B A)
(C B A)
Idio> memq B cpl
(B A)
Idio> memq C cpl
(C B A)
Idio> memq C (memq B cpl)
#f
Idio> memq B (memq C cpl)
(B A)
from which you can see that C
is more specific than B
because
it doesn’t appear in the results of (memq B cpl)
. Alternatively,
B
does appear in the results of (memq C cpl)
meaning it is
less specific than C
.
Of course, this pre-supposes that the CPL of any class is itself a ordered list which is another problem to come.
That now leaves compute-apply-methods
which returns a function
which will take a list of the sorted, applicable methods, as returned
by the application of compute-methods
, and the original args
again.
compute-apply-methods
is the action part but it has a surprisingly
complicated job of its own – something has to implement
call-next-method
. Fortunately it’s been given a sorted list of
applicable methods, what it has to do is massage those into a callable
chain of thunks.
That’s the job of the internal function, one-step
. This is a bit
complicated but let’s ask what it should look like.
We expect that the most specific method is called with the arguments
call-next-method
(bound to a thunk) and args
, the original
arguments to the generic function (all that time ago!).
Invoking the thunk, ie. calling (call-next-method)
, should call
the next most specific method with the arguments call-next-method
(bound to a thunk) and args
, the original arguments to the generic
function (again).
Hmm, it looks like once we jump onto this chain we’ll merrily have the
opportunity to walk down (up?) the tree, if called. How do we jump
on, though? Well, one-step
returns a thunk, so
compute-apply-methods
could simply apply the thunk resulting from
a call to one-step
(with the full list of methods) which will call
the most specific method with call-next-method
(bound to a thunk)
and etc. etc..
one-step
itself, simply checks whether there’s any methods left in
the list and if so applies the function in the method’s procedure
slot with a call to itself, passing the tail of the method list, and
args
.
The call to itself, of course, doesn’t go into some infinite loop but simply returns a thunk, ready to go one step further when it is called in due course.
initialize¶
Tiny CLOS now defines four variants of the generic function
initialize
, as seen above, which is also passed initargs
.
The variants are for:
<object>
which does nothing interesting, just returning theobject
passed inOther variants do a bit more work here as we’ll see below.
<class>
which is expectinginitargs
to optionally have'direct-supers supers
and'direct-slots slots
pairs of argumentsIt then gets busy figuring out values for all the class slots.
There are three more generic functions involved:
compute-cpl
,compute-slots
andcompute-getter-and-setter
. We’ll come back to these but as they are defined as generic functions it means they can be specialized by user-definitions.<generic>
which doesn’t do much, setting the list of methods to#n
and a default instance proc to something reporting that no methods have been defined.<method>
which looks for'specializers specializers
and'procedure procedure
argument pairs
allocate-instance¶
Tiny CLOS defines two variants of the generic function which
only differ by the meta-class of the instance. The body of the
methods runs through the field-initializers
slot of the underlying
class and sets the initial value of the instance’s slots.
compute-cpl¶
Here, I see most implementations veering away from Tiny CLOS in the direction of C3 linearization to determine the class precedence list.
Ultimately, the goal is a deterministic method resolution order which Tiny CLOS effects using the CPL.
The generic function is defined for the specializer <class>
.
compute-slots¶
Here, we’re looking to collect a list of all direct slots and slots of super-classes.
There’s no distinction between identically named slots across classes and the result is just the list of non-duplicated names.
The generic function is defined for the specializer <class>
.
Slot Options I¶
However, the code for compute-slots
seems unexpectedly complex
seeing as it appears to want to just collect distinct slots names from
across the super-classes. It’s not aided by curiously named values
like others
which appear to serve little purpose.
The complexity exists for handling slot options – despite
Tiny CLOS stating at the start of tiny-clos.scm
,
“Classes, with instance slots, but no slot options.”
For a visualization of slot options we can take a peek at the Common Lisp HyperSpec for defclass.
Slot options allow you to augment the nominal slot-name
with
things like (slot-name :initarg keyword :initform func)
such that when you come to initialize an instance of the class you can
specifically override the default value for that slot. (There’s an
obvious implicit :initarg
keyword, that of the slot’s name
prefixed with a colon.)
So that’s cool ‘n all but we hit a problem with multiple inheritance.
What happens if our two super-classes, presumably from different
libraries/authors use the same slot name but different, say,
:initarg
values? Why, you use both, of course!
Wait, though! That won’t work for other slot options such as
:initform
where it doesn’t make any sense to have more than one
(default) initializer.
It isn’t immediately obvious what should happen if both of your
super-classes have legitimately defined an :initform
. Maybe you
take the first (CPL ordered), maybe you raise an error – although if
the libraries are not yours then there’s no obvious fix.
Back to the code. As compute-slots
, or rather, the internal
function, collect
, walks over the list of slots it can pick up on
the head of the list, current
, and that slot’s name
and then
walk over the rest of the list of slots collecting those it hasn’t
seen but also collecting those with the same slot name into others
– although other-similarly-named-slots
might have been a more
descriptive variable name. Or perhaps a comment. Maybe it’s more
obvious to other people.
When we come to update the result
for looping round, we append the
tails of each of the others
lists, ie. just the slot options and
not the slot name, onto current
which has the slot name and the
current slot options.
*
One fault, dare I say it, which you can’t really complain about
because the code has no slot options so I guess it was never tested,
is that when we append the tails of others
we really should be
appending the tails of the reverse of others
.
The reason is that we iterate over the “to do” list in CPL-order and
therefore others
is built up in the reverse of CPL order. If we
append the slot options of the other similarly named slots in reverse
CPL order then we’ll pick up one-shot slot options such as
:initform
in reverse order. For some CPL, we would expect to get
the most specific :initform
, not the least!
Easily spotted and fixed once you implement slot options. No harm done.
compute-getter-and-setter¶
Here, we’re looking to determine the getter and setter per slot. The
arguments are class
(specialized on <class>
), slot
and
allocator
.
allocator
is part of the usual twisty-turney Scheme-y way
of doing things as it is a function to be called with an
initialization function. In Tiny CLOS the initialization
function for the class specializer <class>
simply returns #n
.
The initialization function is added to the list of
field-initializers
and a pair of getter and setter functions
returned which are appended to the slot name to give the per-slot
getters-n-setters tuple of (name getter setter)
.
Slots Options II¶
As Tiny CLOS doesn’t implement slot options it is something of a mystery as to where they are used. Swindle gives us some clues.
Swindle is a much richer implementation so bear that in mind
when looking at tiny-clos.rkt
in the Swindle sources.
Of interest, the Swindle allocator
function returns the
index of the slot. We’ll see why in a moment.
Here, Swindle gathers the various slot-options in the slot
description using getarg
– and getargs
for :initarg
covering the multiple possible :initarg
keywords.
It defines two functions:
init
which is either defined to be:a function invoking
initializer
– another slot option which is a function to initialize the slota function that returns
initvalue
– another slot option, defaulting to???
which is ultimately Racket’s undefined value. I guess#f
would be another possibility albeit you’re in that grey area of uninitialized values – was#f
the class-supplied initial value or is it genuinely uninitialised?
init-slot
a function, passed some variable number ofargs
, to look inargs
for the slot option:initarg
keywords and if not then applyinit
(just defined) toargs
Finally, then, the code can call allocator
with init-slot
and
get back the slot index.
The method also returns g+s
the getter and setter for this slot
which use the slot index (from calling allocator
) to create its
nominal vector-ref
and vector-set!
functions.
*
All well and good, we now have a slot-specific init
function in
field-initializers
. How do we use it?
If someone were to create an initialize
method for some
derivation of <object>
then the onus is on them to either
(call-next-method)
to find their way back here or handle slot
initialization themselves.
When make-instance
(see next section) has created (allocated) an
instance it will invoke the generic function initialize
and in
this case we’re looking to the method for <object>
.
initialize
will have been passed the original initargs
(naming
overload!) arguments to make-instance
which can then call each of
the field-initializer
init-slot
s with initargs
in turn
so that they can seek out their :initarg
(s) arguments etc..
make-instance II¶
Now that we’ve defined all the generic functions for the MOP we can
combine them all together into the final definition of
make-instance
(make
in Tiny CLOS):
(set! make
(lambda (class . initargs)
(let ((instance (allocate-instance class)))
(initialize instance initargs)
instance)))
where we allocate an instance of the supplied class
with the
generic function allocate-instance
before initializing the
instance with the generic function initialize
passing the supplied
initargs
.
And Boom! A multi-methods object system is born.
Native Types¶
There’s a final nicety to provide a bit of completeness. We can define a bunch of classes representing the native Scheme (or Idio) types, say fixnum, bignum, string, etc..
We can augment the class-of
function which would normally return
the meta-class of an instance to check for each of the native types,
fixnum?, bignum?,
string?, etc. and return the corresponding type.
This now means that you can have a specializer of <fixnum>
,
<bignum>
, <string>
, etc. and pass 1
, 1.0
, "1"
,
etc. as arguments for the right thing to happen.
Enhancements¶
STklos¶
STklos looks to add a reasonable chunk of the bootstrap in C
(see src/object.c
). Whilst it supports the
compute-apply-generic
principles (called apply-generic
and
similarly in lib/object.stk
) the code for actual <generic>
instances is handled in C. Which seems like a good thing as
(looking ahead) the pure Idio mechanism is quite slow.
STklos also plays some tricks with the getters and setters.
In the first instance it uses a (name init-func getter)
tuple where getter
defaults to being the (integer) index of
the slot. When it comes to accessing the slot it can test to see if
the getter is an integer and if so access the slot directly otherwise
call the (presumed) function.
Swindle¶
Swindle extends the initializers to allow for more useful, er, initialization.
It also supports the wider CLOS-like system of primary methods to include before, after and around methods.
Problems¶
Tiny CLOS does suffer from some problems. A search for Swindle documentation may well end in frustration as has disabled access to the documentation, none of which was transposed into any other form previously.
He has noted some problems with Swindle and related technologies in that generic functions have a shared state. If there’s only you editing code it’s not a problem but when one or more libraries are affecting generic functions and one makes a mistake it has the potential to take down all the rest.
Both of these are in the first reply in this Reddit thread Why is Swindle hidden away?.
In the follow-up he makes another interesting point:
The work that I referred to might be possible. I’ve heard ideas about generics that instead of being mutated they’re being extended, so when you do a defmethod, your own version of the generic is extended, but not my version, and therefore such horror stories are gone. Doing something like that would be wonderful, IMO, but nobody really tried to make it happen.
That’s an intriguing idea. In essence it would require that generic functions become localized to modules. That, in turn, would mean you could not build on the work of others.
Last built at 2024-12-21T07:11:02Z+0000 from 463152b (dev)