A shape class (the base class)
various subclasses (circle, rectangle etc. etc.)
and some mixins like
color-mixin (adds color information)
printable-mixin (adds data needed to print the object on paper)
active-mixin (adds callbacks for handling events on the object)
...and so on. Now by means of multiple inheritance I can combine the
mixins to get exactly the functionality I want (slots + methods). But,
this can lead to proliferation of classes which are really used just
as a combination of mixins, i.e. they don't add functionality but just
pack it under a name:
(defclass active-colored-circle (circle color-mixin active-mixin) ())
(defclass printable-colored-rectangle (rectangle color-mixin printable-
mixin) ())
et cetera.
Since I realised I was doing this repeatedly, and I was using such
classes only once or twice, I thought about using "throwaway classes"
instead - that is, combining mixins without actually creating a class
with a given name. This is what I did:
(defmacro make-object (superclasses &rest initargs)
`(make-instance (defclass ,(gensym "THROWAWAY-CLASS") ,superclasses
())
,@initargs))
then I can do (make-object (circle color-mixin active-mixin) :radius
10 :color :green :callback #'some-fn)
Is this the "right" way to do what I want? Or maybe you can achieve
the same effect using the MOP in a simple(r) way? Or, instead, is this
a solution to the wrong problem?
Any input is appreciated :)
cheers
Alessio Stalla
> (defmacro make-object (superclasses &rest initargs)
> `(make-instance (defclass ,(gensym "THROWAWAY-CLASS") ,superclasses
> ())
> ,@initargs))
>
I think your throw away classes will never be thrown away, they won't be
garbage collected.
Sacha
If you want to stick to that approach, please note that whenever you
create an instance with make-object, you will always get a new class,
even if the same combination of mixins had already been used before.
This creates a considerable overhead.
Further note that such throwaway classes are never garbage collected,
because the CLOS MOP specifies that a class should have a reference to a
list of all its subclasses, and that is not specified as a weak pointer
(which I think is an oversight, due to the fact that the concept of weak
pointers doesn't exist in ANSI Common Lisp).
You could probably compensate for these problems using the CLOS MOP: You
could define a better make-object that caches combinations of mixins,
and you could probably even define a metaclass that hooks into the
garbage collector to ensure that classes without instances are
automatically removed.
However, I think that would be overkill.
Why don't you just factor out some of the functionality into common
superclasses that are always inherited from? CLOS provides the notion of
unbound slots, and that's a pretty straightforward way of indicating
that some feature is not present. This creates a certain overhead,
because all instances will always waste storage on features that aren't
used, but overall, the resulting class hierarchy would be much simpler...
I guess that Kenny will have an even better idea... ;)
Pascal
--
My website: http://p-cos.net
Common Lisp Document Repository: http://cdr.eurolisp.org
Closer to MOP & ContextL: http://common-lisp.net/project/closer/
I think the use of mixins is best used for the (internal) implementation
part of a certain piece of code. Best not to expose it to the
user of some 'library'. I would expose only general purpose
classes which probably inherit from more mixins than needed.
Say you would write a BASIC-WINDOW class and then some
mixins (BACKING-STORE-MIXIN, BORDER-MIXIN, TRANSPARENCY-MIXIN, ...).
For the user there would be a STANDARD-WINDOW class which
inherits from BASIC-WINDOW and uses all the basic useful mixins and, say,
a class EXTENDED-STANDARD-WINDOW which provides all the features
you can imagine ;-) .
In some old system I saw an architecture where you
could add or remove classes to the superclasses
of a certain object at runtime. That would make
sense in some 'configuration' tools.
You'll find almost exactly this example in AMOP. Sample code that
works at least in SBCL (thanks to Christophe Rhodes' efforts):
<http://www.foldr.org/~michaelw/lisp/amop-programmatic-class.lisp>
Cheers,
Michael
> (defmacro make-object (superclasses &rest initargs)
> `(make-instance (defclass ,(gensym "THROWAWAY-CLASS") ,superclasses
> ())
> ,@initargs))
>
> then I can do (make-object (circle color-mixin active-mixin) :radius
> 10 :color :green :callback #'some-fn)
>
> Is this the "right" way to do what I want? Or maybe you can achieve
> the same effect using the MOP in a simple(r) way? Or, instead, is this
> a solution to the wrong problem?
As others have pointed out, these classes will never be collected
because they have references from their superclasses, at least.
Worse than that, you will probably end up with multiple classes which
are operationally identical (they mix the same classes) but are not,
in fact, the same class.
That being said, I don't think what you're trying to do is stupid, and
I have done similar things. The trick is to make your class-mixing
macro/function only create the class if it does not already exist. So
it needs a secret cache of classes it has already mixed for you and
should return a preexisting class if it can.
To do it right you need the MOP I think.
Somewhere I have code that does this but I am not sure where and it is
probably no good anyway - it was all more than 10 years ago I think.
--tim
Thanks again!
A.
Robert Strandh et al. have a paper about just that:
"Using Stealth Mixins to Achieve Modularity" <http://dx.doi.org/
10.1109/ASWEC.2007.52>
(Robert might have a version without ACM tax somewhere)
Pascal Costanza's ContextL is related to that idea, too:
<http://common-lisp.net/project/closer/>
Cheers,
Michael
i think it's better to define class only once for each combination -- not
each time make-object is called, and not each time it gets macroexpanded or
whatever. while it still might be a bit suboptimal comparing to MOP
solution, i believe there would be no practical difference.
something like this:
(defmacro make-object (superclasses &rest initargs)
(let ((classname (intern (format nil "~{~a+~}" superclasses))))
`(make-instance (or (find-class ',classname nil)
(defclass ,classname ,superclasses
()))
,@initargs)))
Wearing my maintenance programmers hat, this distinction,
between "throwaway classes" and "real classes" sounds like
quite a window into how the original programmer was
thinking. Any particular factorisation of the problem into
orthogonal classes probably appears to be the only possible
one, once you have thought of it, but others may approach
your code with clashing expectations and this is the
opportunity for the original programmer to spell out his
vision.
A big block comment, talking at a high level about the
application domain, what it is about this application that
gives rise to the distinction, would be solid gold.
Don't despise the DEFCLASSes of the throwaway classes
(defclass active-colored-circle
(circle color-mixin active-mixin)
()
(:documentation "Throwaway"))
would relate back to the big block comment, re-assuring the
maintainer that he is understanding the code.
Alan Crowe
Edinburgh
Scotland