There’s been some work done on Sheeple’s MOP, and I figured I’d write a bit about what got done today…
The original reason for writing Sheeple, like I said before, was to use it with Sykosomatic (which I still haven’t written an entry for, meh). That said, Sykosomatic’s object system needs to be fully persistent. Sheeple itself is not persistent — it works like a standard run-time object system.
Because I’m a masochist and a nerd, I figured I’d simply write Sheeple as the regular object system that it is, then “just” add a Metaobject Protocol like CLOS’ once the base language was set. Ho boy, did I ask for trouble. MOPs are a royal pain in the ass to do properly, and they’re a bit mind-bending. Regardless, Sheeple has been slowly getting MOP features over time. The neat part? The MOP itself is written in CLOS (as are a lot of Sheeple’s internals).
What I managed to get done today is the protocol for manipulating properties and property behavior through property metaobjects.
To show how it works, let’s go over an example of how one might implement persistent-sheeple, so maybe the damn thing can actually be used for Sykosomatic…
(in-package :sheeple-user)
(defvar *max-oid* 0)
(defclass persistent-sheep (standard-sheep)
((oid :initform (incf *max-oid*) :reader oid)))
(defclass persistent-property (standard-property) ())
The first step to changing behavior is to make some subclasses. We’ll be dispatching on these.
(defmethod initialize-instance :after ((sheep persistent-sheep) &key)
(allocate-sheep-externally sheep))
(defmethod add-parent :around ((new-parent standard-sheep)
(child persistent-sheep))
(if (or (eql new-parent =dolly=)
(eql (find-class 'persistent-sheep)
(class-of new-parent)))
(progn
(call-next-method)
(add-parent-externally new-parent child))
(error "We don't support adding non-persistent~
sheep as parents!")))
The first and most important part of having a persistent object is that it’s somehow allocated externally. All Sheeple are standard CLOS instances, so this method will get called each and every time the sheep object is created.
We also go ahead and write a method for add-parent. This is important because we want to make sure that we can successfully serialize the sheep. We must always have at -least- =dolly= as a parent, so we have to make an exception for that. Other than that, no non-persistent sheep are allowed into a persistent-sheep’s hierarchy list. Note that methods defined on add-parent must not override the standard behavior (basically, the (standard-sheep standard-sheep) method must always be called). Other than that, clients are free to go wild with what they do with this one.
With this much code (aside from serialization and db-management stuff!) we can now have externally-stored sheep objects. The rest of sheeple will work normally at this point. The whole point of persistent-sheeple, though, is to be able to store property values externally, so here we go…
(defmethod add-property :around ((sheep persistent-sheep) pname
value &key transientp)
(register-property-externally sheep pname)
(if transientp
(call-next-method)
(call-next-method sheep pname value
:property-metaclass 'persistent-property)))
(defmethod add-property-using-property-metaobject :before
((sheep persistent-sheep) value
(property persistent-property) &key)
(allocate-property-externally sheep pname))
Because Sheeple requires that properties be specifically added to the hierarchy-list, we have these two functions: The first one takes care of creating a property metaobject for the second one to dispatch on. In this case, we make our :around method on add-property accept a keyword that tells it whether a property should be transient or not.
For the purpose of this exercise, we assume that a transient property is simply a regular property on a persistent-sheep. Since we want to make sure we’ve got the property available when we restore the database, though, we register its existence regardless of whether it’s transient or not.
Next up, in a-p-u-p-m, we actually dispatch on the property object itself, instead of just the class. This :before method sits there to make sure we have external DB space to allocate a value on, since we’re sure in this case that we’ll be assigning actual values to this one.
Once that’s set, it’s just a matter of making sure we can get/set those values, and we’re home free…
(defmethod direct-property-value ((sheep persistent-sheep)
(property persistent-property))
(get-property-value-externally sheep (property-name property)))
(defmethod (setf property-value) (new-value (sheep persistent-sheep)
(property persistent-property))
(set-property-value-externally sheep (property-name property)
new-value))
And that’s it. We override the standard getting/setting behavior so nothing is set locally: Storage lives in the database.
Keep in mind here that a built-in :before method for (setf property-value) will take care of calling add-property as appropriate when the sheep object does not have a direct property metaobject registered for that property-name.
Now we can actually use it:
(defproto =psheep= ()
((foo "bar")
(baz "quux" :transientp t))
(:metaclass 'persistent-sheep))
(note: defproto changed a bit in recent commits, more on that later…)
And there you have it! (setf (foo =psheep=) "Blargh") is all it takes to start changing stuff in your new persistent sheep. The rest of sheeple will work as expected. It’s worth noting that roles belonging to this object won’t be persisted, since we only messed around with object creation and properties.
Discussion
While I’m very pleased that the MOP has come this far, I have to admit that it probably doesn’t work very well right now. It’s very new, and very much still a work-in-progress. There’s probably certain things that could be cleaned up and all that, but I think it’s certainly a step in the right direction.
I’m honestly not entirely sure what I’m doing half the time. Even though I’ve read through AMOP several times, some of this is still a bit confusing, and I haven’t actually used CLOS’ MOP very much at all. I’m playing it by ear (with a hell of a lot of help from the debugger!)
Hopefully, as the MOP grows (and evolves), it’ll settle into something generally useful. For now, though, YMMV if you want to mess with it.