Sunday, October 30, 2005

For those of us who have never heard of a MetaObject Protocol, the MOP for CLOS is a bit puzzling. What exactly is it? It's a standard that allows you to customize and extend the common lisp object system. For example, if you want to map objects onto databases like Class::DBI in Perl, or you want object persistance like Java's Prevayler, the MOP is the lisp machinery that will allow you to do it. Before one can delve into the dank cave of MOP, it helps to orient oneself a bit, and see how the CLOS is really put together. I'm using clisp, so if you run these examples in a different lisp, you may see different results. First, let's see what classes are made of. [1]> (defclass myclass () ((a :accessor mc-a))) #<standard-class> [2]> (class-of (make-instance 'myclass)) #<standard-class> There's nothing surprising about this. The class of a myclass object is #, just like we suspected. But here's something novel, classes in common lisp are first-class objects. What's the class of MyClass? [3]> (class-of (find-class 'myclass)) #<standard-class> Interesting. What this should tell you is that the class MyClass is an instance of the class Standard Class. Do not make the mistake of thinking that MyClass is a SUBCLASS of Standard-Class! This is emphatically NOT the case! [4]> (subtypep (find-class 'myclass) (find-class 'standard-class)) NIL ; T Compare this to [5]> (subtypep '(integer 0 2) 'integer) T; T Which tells us that yes, the integer range from 0 to 2 is indeed a subtype of integers. So what's going on here? Why isn't MyClass a subclass of Standard-Class? In MOP parliance, Standard-Class is the metaclass of MyClass. You can do all sorts of wonderful and strange things using metaclasses, but let's hold off on that for a bit. The next interesting thing to find out is that the slots of a class are themselves objects. Not just ordinary places that can be SETFed, but real, full-fledged objects of their own right, with inheritance, subtyping, and metaclasses of their own. Let's explore. [6]> (class-direct-slots (find-class 'myclass)) (#<standard-direct-slot-definition>) One may wonder at the awkward verbosity of MOP, it certainly can cause hand cramps. Why class-direct-slots and not class-slots? It so happens that class-slots already exists. [7]> (class-slots (find-class 'myclass)) (#<standard-effective-slot-definition>) What gives? There's an important distinction in MOP between direct slots and effective slots. Direct slots are the objects that are created when the slot is first created. For example, A is a direct-slot in myclass. The difference comes into play when inheritance happens: instead of inheriting the slot objects themselves, copying them straight into the subclass, the MOP allows one to combine the slot definitions for the superclass and the subclass. This makes a lot of sense when you consider what happens when you have (defclass MyOtherClass (MyClass) ((a :accessor moc-a))). Remember that slot A was once accessible using mc-a. What accessor should fetch the slot value of A in MyOtherClass objects? Should it be mc-a or moc-a? Using an effective-slot-definition, you can allow both. To make this more real, let's have an example. Everyone who's used CLOS is aware that CLOS doesn't support encapsulation directly. Even if you supply no accessors for a slot, someone can always read and write the slot using slot-value. Since slot-value is a function, there's no way to override the behavior of slot-value using defmethod. But there is another way, using the MOP. Conceptually, slot-value is defined to call slot-value-using-class, which is a generic method. In many lisps, slot-value is optimized when slot-value is called on an object whose class has a metaclass of standard-class. So we'll need to make a metaclass in order to define a method that can intercept calls to slot-value-using-class. [8]> (defclass my-metaclass (standard-class) ((class-var :accessor mc-class-var :initarg :class-var))) #<standard-class> We next need a class of metaclass My-Metaclass so we can make objects to call slot-value on. [9]> (defclass my-other-class () ((instance-var :accessor moc-instance-var :initarg :instance-var)) (:metaclass my-metaclass)) #<my-metaclass> Then we can specialize slot-value-using-class. [10]> (defmethod slot-value-using-class :before ((class my-metaclass) instance slot) (print 'fetching-slot-value)) Now, make an instance and get the slot's value. [11]> (setf inst (make-instance 'my-other-class :instance-var 'hello)) [12]> (slot-value inst 'instance-var) FETCHING-SLOT-VALUE HELLO This should instantly suggest several interesting things. For example, one could replace slot-value-using-class instead of wrapping it, and thereby get computed slots! This would be useful if your class represented a database table and the slots represented columns. Suppose you had a BLOB column that you didn't want to transfer unless it was requested. By wrapping slot-value-using-class, you could fetch the BLOB if it wasn't already present. There are lots more interesting things you can do with the MOP. I'll get around to giving some more concrete and followable examples. Mostly, I wanted something to be online for googlers who had as much trouble learning about MOP as I did.


Post a Comment

<< Home