[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

MOP Comments (Repost)



The following are a compendium of comments from myself, Andreas Paepcke
(who has written a PCL metaclass for persistent objects) and Warren
Harris. I've also included the full text of Warren's commentary, at his
request, after the summary:

1) mixin v.s. common-base

Actually, the mixin style seems better than the common-base style, since
it partitions the state and method protocols more orthogonally, and thus
potentially avoids duplication. This seems to be moving in the direction of
method protocol classes, and classes which implement state for the
protocol. The protocol class would simply define the "external" (meaning
required to be supported) operations of the protocol, and subclasses of
the protocol class would supply specific methods (and perhaps additional
slots) which implement that protocol. In this way, it is possible to have
several implementations of a protocol and simply use type or class
predicates to test whether an object supports a protocol.

The key problem is, as Gregor noted, partitioning the protocols so that
state and function reflect what people need to extend an object-oriented
system. As Danny pointed out, it may be difficult at this time to do it
right. We already have some experience, via PCL, with the common-base
style and so can probably make a better estimate of what is needed. In
addition, Danny's comment about Pierre Cointe is relevent, since it would
be good if the ISO/EuLisp community could agree to what we come up with.

Finally, and most importantly, Danny's comment about designing an 
object-oriented system and grafting Common Lisp on top is very appropriate.
With what we know now, we could certainly do better, but I don't believe
we have that option.

2) "class-prototype"

There is no explicit definition of what a class prototype is in the MOP.
Why should a class prototype be any different than an instance of a
class object of the particular metaclass required?

3) Classes in the CLOS Kernel

There is no slot for the dispatcher code in STANDARD-GENERIC-FUNCTION.
Is the idea to have different method dispatch protocols implemented by 
specializing STANDARD-GENERIC-FUNCTION, with COMPUTE-DISCRIMINATOR-CODE 
returning the specialized code? The problem with this is that, for 
optimization purposes, it may be desirable to have the dispatcher on a 
STANDARD-GENERIC-FUNCTION instance change. For example, if all the methods 
on the generic function only have built-in classes as parameter specializers, 
then the dispatcher need not check if the class of the arguments is a user 
defined class, since, if it's not a built in class, then an error should be 
signalled. This avoids the overhead of checking whether the arguments are
of a user defined class. Given the MOP as outlined in the document, the
only way this could be achieved is to have a different kind of generic
function, and that means that the generic function object would not be
EQ before and after a method which has user defined classes in the
parameter specializer list is added. One way to fix this would be to do a
CHANGE-CLASS  on the generic function object, but that seems somehow
to be less than optimal, since it seems to be just fixing up the problem
afterwards.

4) Editorial Comment: Location of Named Class Definition, Slot Parsing,
   and Inheritance Sections

These three sections should probably be moved so that they are in closer
proximity. Perhaps even making the slot sections a subset
of a section on named class definition, since they must logically precede
class definition. Most helpful would be a 1-2-3, step by step description
of what happens when in class defintion, and which generic functions are
involved.

5) Class and Method Definition Syntax

There is a fundamental tension in the document between the desire to
have DEFCLASS and DEFMETHOD work for user defined metaclasses 
as well as the defaults, and the lack of the ability to do certain things.

Two examples:

a) Suppose I want to define a kind of slot which asks the user for a
value upon access (ASK-SLOT). Many AI languages have this kind of thing.
One way to do it would be to add a :SLOT-CLASS option to the slot-spec
syntax, but there are likely to be other areas where users will want
to make semantic extensions to class definitions, which will then need
to be reflected in the syntax.

b) Paragraph 6, pg. 3-19 implies that it is only possible to have one
method class per generic function. Presumably, a DEFGENERIC would have
to be evaluated before any method definition, in order to establish
a generic function which has a different method metaclass from the 
default. There are two problems with this. One is that a generic function
is created automatically when a method is defined, but the method class
is always the default. This establishes a certain asymmetry between the
syntax of defining a method and generic function with a user defined
method metaclass and defining a class with a user defined metaclass, 
and could lead to mysterious errors, should a user later
try to add a method of a user defined metaclass to an existing generic
function with methods of the default type. Secondly, it is not possible
to have a generic function with methods of more than one method class.

This tension can be resolved in a number of ways:

1) Drop the :METACLASS option for DEFCLASS and :METHOD-CLASS option for
DEFGENERIC. Thus, if people want to implement metaclasses requiring
syntactic changes will have to supply their own macros to do it. The generic
functions in the MOP should be sufficient to support this, however, and
the procedure for doing it should be outlined. The disadvantage would
be syntactic divergence between CLOS syntax and that used by implementors
of other metaclasses (but see the section below on how to handle
the default CLOS metaclasses for a possible mitigation).

2) Extend the MOP for DEFCLASS parsing and the DEFGENERIC syntax to handle
the cases outlined above (and others which could potentially be found).

Specifically, addressing a), functions for registering permitted syntactic
extensions to the DEFCLASS parser are needed. This would provide a limited
ability to extend the DEFCLASS syntax, in areas where users are most likely
to want it. Here's a shot at a specification:

ADD-DEFCLASS-SLOT-OPTION _metaclass_ _option_

Causes _option_ (a symbol) to be registered with the DEFCLASS parser
as a valid slotd option for classes of metaclass _metaclass_ (a class
object). The option will be added to the normalized slot specification
during slot parsing.

ADD-DEFCLASS-CLASS-OPTION _metaclass_ _option_

Causes _option_ (a symbol) to be registered with the DEFCLASS parser
as a valid option for classes of metaclass _metaclass_ (a class object).
The option will be added to the _options_ parameter of EXPAND-DEFCLASS.

Addressing b), the :METHOD-CLASS option of DEFGENERIC could be extended to 
take a list, but this would not solve the problem with DEFMETHOD creating a 
generic function having the default method class, since now the method
class would have to somehow be specified in DEFMETHOD. Another possiblility
would be to add EXPAND-DEFGENERIC-OPTIONS to the MOP:

EXPAND-DEFGENERIC-OPTIONS	((class standard-class)
				  name lambda-list options)


6) Code for Class/Method Definition

Why is it necessary to have a special function, ADD-NAMED-CLASS, which
instantiates a class? And ADD-NAMED-METHOD, for methods? Since classes
and methods are objects, why not simply use MAKE-INSTANCE, and put the
special processing into the INITIALIZE method? For example, the return
code for class definition would look like:

`(progn
   (eval-when (compile load)
	(make-instance ',(class-name (class-of prototype-instance))
	  :name ',name
	  :direct-superclasses ',direct-superclasses
	  :direct-slots ',direct-slots
	  :options ',options
	  :environment ',environment
	)))

Metaclass users would need to implement INITIALIZE methods, but could
use CALL-NEXT-METHOD and customize.

7) Miscellaneous Class/Slot Definition

The specification of the SLOT-DESCRIPTION-CLASS generic function is
somewhat vague. By (pg. 3-21, paragraph 7):

	This SLOT-DESCRIPTION-CLASS generic function is called with the
	class and the normalized slot specification to determine the
	class of slot description which should be produced for the class.

Does this mean that it returns the class object, so that MAKE-INSTANCE
can be called in the next step?

If so, then additional slot options should be hookable by defining
methods on SLOT-DESCRIPTION-CLASS for other metaclasses, or on individual
classes, if the metaclass  is STANDARD-CLASS, as long as the syntax can
be customized.

As far as class options are concerned, what is the format and processing
sequence for DEFCLASS options? Are they passed down to EXPAND-DEFCLASS
in a list, as they appear at the end of the DEFCLASS form? Or is some
additional parsing done? If they are simply passed down as a list, with the
exception of the :METACLASS option which must be fully parsed to
instantiate the class prototype, the parser need only check if the option
is permissible. If other parsing is needed, a generic function, 
EXPAND-DEFCLASS-OPTIONS, should be considered, and have a structured format
for passing the information to EXPAND-DEFCLASS. Parsing of class options
could just as well be done in EXPAND-DEFCLASS, thus centralizing class
definition in one place, but it is important that this be explicitly stated 
in the document.

Re: comment about :SLOT-INITFORM-FUNCTION in the middle of pg. 3-21, 
the logical place to do this is during the initialization of the slotd
object, during the MAKE-INSTANCE of the slotd object.

8) Slot Inheritence Protocol

I think an additional generic function will be needed to resolve
inheritance between slotd objects of different classes. 
COMPUTE-EFFECTIVE-SLOTD calls this generic function on each
adjacent pair of slotds in the slotd list. The function must either
return one of the slotds, create another resolving the conflict, or
signal an  error, if resolution is not possible. Here's a try at
an interface:

RESOLVE-SLOTD-CONFLICT ((class standard-class)
			(less-specific-slotd standard-slot-description)
			(more-specific-slot-description standard-slot-description))

Is there any other processing COMPUTE-EFFECTIVE-SLOTD must do besides 
resolving pairwise conflicts? If not, then perhaps we only need 
RESOLVE-SLOTD-CONFLICT, and COMPUTE-EFFECTIVE-SLOTD could remain hidden.

9) Way of Changing the Default Metaclasses

Some way of changing the default metaclasses for DEFCLASS, DEFGENERIC,
and DEFMETHOD is needed, so that users can simply change them when
loading a file. This would also eliminate the need for the :METACLASS
class option, although the user would have to accept the default syntax.

Two possible ways are:

a) A set of global variables, *DEFAULT-METACLASS*, *DEFAULT-SLOT-CLASS*,
*DEFAULT-GENERIC-FUNCTION-CLASS*, and *DEFAULT-METHOD-CLASS*.

b) A set of access functions which get/set these defaults in the global
environment. The function to set them could be a SETF function.

10) Protocol for Reader/Writer Functions is vague

One piece of metaobject programming which has become vital in PCLOS
(persistent CLOS) is the ability to intercept slot access, including
slot access through generated accessor functions. In the current PCL
implementation, alot of effort was needed to find out what 
MAKE-READER-METHOD-FUNCTION and MAKE-WRITER-METHOD-FUNCTION do. Some
level of the slot access function's creation should be detailed, so
that metaclass programmers can customize these functions.

11) Accessing Shared Slots

Currently, the only way to access shared slots is through instances. This
leads to the cumbersome necessity of having to create an instance to
do something with class allocated slots. There should be a way to get
at this information in the metaobject protocol, so that user defined
metaclasses don't have to create instances.

12) Moving from One Level to Another in the Slot Access Protocol

The formalization of slot access levels is good, but there is a need
for a way to go from one level to another. For example, there should
be a method for getting the storage index from the slot name, given
the class, or to get a slot description from a slot index. These
methods were implemented for PCLOS, and were not hard to do, but they
seem to be logically part of the metaobject protocol.

13) Instance Structure Protocol

To motivate the following discussion, consider the following situation. I
currently have a user who is thinking of using the MOP to define a new
metaclass (C-METACLASS) which will allow creation of objects outside
of the Lisp address space. Now our garbage collector does compaction, so
Lisp objects can move when garbage is collected. This won't work for C,
but we can use bit vectors to put C objects in, since the garbage
collector doesn't look at the contents of those. The obvious way of 
creating an instance of C-METACLASS is to use a bit vector with a tag in
the first word, and the C data (or pointers thereto) in the rest.

The problem with the proposed instance structure protocol is that I can't
make instances from bit vectors. Instead, I have to use one level of
indirection, making up instances from STANDARD-INSTANCE which slows down
slot access, because I need to do an additional level of indirection to
get at the data. The optional STORAGE-INFORMATION argument is insufficient,
because the entire instance must be made from a different primitive
structure than a standard instance is.

Why not arrange for the instance access and CLASS-OF protocol to accomodate
user defined instances? Here's a suggestion for a protocol:

DEFINE-METACLASS _class_ _metaclass-p_ _class-of_

Registers class _class_ as a new metaclass. _metaclass-p_ is a function
which returns T if an instance is of metaclass _class_, and _class-of_
is a function which returns the class of the instance.

STANDARD-INSTANCE-CLASS-OF _object_
STRUCTURE-INSTANCE-CLASS-OF _object_
BUILT-IN-CLASS-OF _object_

Do the obvious.

In order that instances of metaclasses other than STANDARD-CLASS can be
made from standard instances, structure instances, or anything from the
built-in types, CLASS-OF needs to check first if an instance is of a
user defined metaclass, before checking if it is one of the default
instances.

14) Instance Access Optimiztion Protocol

This should be carefully documented, as it was difficult in the PCL system
to figure out what to do.

The thought behind it is good, but the italicized comment in the middle
of pg. 3-37 sums up the problem. Currently, no way exists to tell in
Common Lisp if a particular function is executing as part of the compiler
or not. There are two ways of dealing with this (probably more):

a) The _context_ argument becomes an environmnet, and Common Lisp provides
some way of distinguishing the compile time environment. Unfortunatly,
the power in this could potentially lead to code which for which the
interpreter and compiler don't have the same semantics, contradicting one
of the design goals of Common Lisp.

b) We simply specify that all compilers must call OPTIMIZE-INSTANCE-ACCESS
at a particular optimization level, but interpreters need not. This
provides a less powerful hook, but relies on convention.

15) FORWARD-REFERENCE-CLASS

I agree with Danny and Gregor. There are some methods one may want to
define on FORWARD-REFERENCED-CLASS, if nothing other than to warn the
user that a forward referenced class is being created. Having a class
makes it more modular, since the code is concentrated in the class's
method protocol.

***********************  Warren's comments follow ******************************


My comments on the MOP (Meta-Object Protocol)
Submitted by:  Warren Harris, HP Labs (harris@hplabs)


1.  Nowhere does the MOP mention what this notion of a "class-prototype"
is, or why it is essential to CLOS.  If it is essential, it should be
documented.  If not, the specification should not be written in terms of
it.  (I really think the whole notion of class prototypes should be striked
from the MOP, and viewed as an implementation detail.)


2.  I still fail to see the need for special meta-object defining methods.
Why can't things like classes and methods be created by MAKE-INSTANCE, and
allow the INITIALIZE method on the meta-object to initialize that object
properly.  Here is a fragment of code that I believe should work:

(defclass foo () (this))

(make-instance 'class
  :name 'bar
  :direct-supers (list (class-named 'foo)))

(make-instance 'bar :this 3)

In this example, everything that is necessary to make BAR a valid CLOS
class has been taken care of by the STANDARD-CLASS INITIALIZE method.  This
includes computing the "effective" slot descritions (according to the
inheritance specified by COMPUTE-EFFECTIVE-SLOT-DESCRIPTION), registering
the class name on the class name table, and redefining any existing class
of the same name.  (P.S.  The STANDARD-CLASS INITIALIZE method should also
allow a :ENVIRONMENT init keyword, passed through by the MAKE-INSTANCE
call.)


3.  I wonder if it is necessary for the extensive mechanism that expands
DEFCLASS and DEFMETHOD macros.  It seems to me that there are two ways you
can view these macros: (a) as a top-level interface to creating instances
of STANDARD-CLASS and STANDARD-METHOD with no accomodation for additional
syntax that may be needed by subclasses of these meta-protocols (a user
would define new macros for radically different class creation), or (b) as
"generic" macros which may be customized to meet any meta-protocol for
defining specialized classes or methods.  While the currently proposed
EXPAND-DEFCLASS method supports (b), I think that we can get a lot of
milage out of option (a) provided the macro expansions for STANDARD-CLASS
and STANDARD-METHOD are general enough.  I would propose the following:

(defmacro defclass (name supers slots &rest options)
  (let ((metaclass (or (getf options :metaclass)
		       *default-metaclass*)))
    (remf options :metaclass)
    (when (or (getf options :name)
	      (getf options :direct-supers)
	      (getf options :direct-slots))
      (error "Invalid option in defclass."))
    `(make-instance ',metaclass
       :name ',name
       :direct-supers (list ,@(mapcar
			       #'(lambda (super-name)
				   `(class-named ',super-name))
			       supers))
       :direct-slots
       (list ,@(mapcar #'(lambda (slot-spec)
			   (let* ((slot-name (first slot-spec))
				  (slot-options (rest slot-spec))
				  (slot-class (or (getf slot-options :slot-class)
						  *default-slot-class*)))
			     (remf slot-options :slot-class)
			     `(make-instance ',slot-class
					     :name ',slot-name
					     ,@slot-options)))
		       slots))
       ,@options)))

Example:

(defclass workstation (computer commodity)
  ((manufacturer :initform "HP")
   (model-number)
   (mips :slot-class askable-slot))
  :metaclass p-class
  :documentation "A workstation.")

expands to:

(make-instance 'p-class
  :name 'workstation
  :direct-supers (list (class-named 'computer)
		       (class-named 'commodity))
  :direct-slots (list (make-instance 'standard-slot
				     :name 'manufacturer
				     :initform "HP")
		      (make-instance 'standard-slot
				     :name 'model-number)
		      (make-instance 'askable-slot
				     :name 'mips))
  :documentation "A workstation.")

Again, the STANDARD-CLASS INITIALIZE method would take care of what
ADD-NAMED-CLASS is doing now.  In this defclass macro, the only reserved
words are the symbols :metaclass and :slot-class.  All other options and
keywords like :initform or :accessor are simply attributes of the instances
being created.  Advantages: init-plists of subclasses of standard-class and
standard-slot can be passed through the defclass form naturally.
Disadvantages: not enough protection is given to the make-instance calls
because the user could supply values to slots which should not be initable.

The second method for expanding defclass would be to go through one level
of indirection, through a generic macro-expansion function.  For this I
propose something similar to what is already described, but with two levels
of parsing rather than the slot-specification normalization technique.
This way both the metaclass and the class of each slot may affect the
parsing process.

Rather than use the SLOT-DESCRIPTION-CLASS method to determine from the
normalized slot-specification the slot-descriptor class, it standardizes on
the :SLOT-CLASS keyword to extract the slot-descriptor class.  (Was this
normalization process supposed to be done by the EXPAND-DEFCLASS method?
If so, users would have to reimplement that everytime they specialized
expand-defclass.)  Rather than "normalizing" a slot specification, another
generic function, EXPAND-SLOT-SPECIFICATION, is called to create forms that
will make slot-description objects:

(defmacro defclass (name supers slots &rest options)
  (let ((metaclass (class-named (or (getf options :metaclass)
				    *default-metaclass*))))
    (remf options :metaclass)
    (expand-defclass metaclass name supers slots options)))

where the standard-class expand-defclass method would be:

(defmethod expand-defclass ((c standard-class) name supers slots options)
  (when (or (getf options :name)
	    (getf options :direct-supers)
	    (getf options :direct-slots))
    (error "Invalid option in defclass."))
  `(make-instance ',(class-name c)
     :name ',name
     :direct-supers (list ,@(mapcar
			     #'(lambda (super-name)
				 `(class-named ',super-name))
			     supers))
     :direct-slots
     (list ,@(mapcar #'(lambda (slot-spec)
			 (let* ((slot-name (first slot-spec))
				(slot-options (rest slot-spec))
				(slot-class (class-named 
					     (or (getf slot-options :slot-class)
						 *default-slot-class*))))
			   (remf slot-options :slot-class)
			   (expand-slot-specification c
						      slot-class
						      slot-name
						      slot-options)))
		     slots))
     ,@options))

and another generic macro expansion (multi-)method would reside on
standard-slot-description to continue the expansion of the defclass:

(defmethod expand-slot-specification ((c standard-class)
				      (s standard-slot-description)
				      name
				      options)
  (let ((reader (getf options :reader))
	(accessor (getf options :accessor)))
    (remf options :reader)
    (remf options :accessor)
    `(let ((slotd (make-instance ',(class-name s)
		    :name name
		    ,@(when reader
			`(:readers '(,reader)))
		    ,@(when accessor
			`(:accessors '(,accessor)))
		    ,@options)))
       ,@(when reader
	   <code to create reader method>)
       ,@(when accessor
	   <code to create accessor method>)
       slotd)))

(I'm not sure, but maybe these reader and writer methods should get
generated by the STANDARD-SLOT-DESCRIPTION INITIALIZE method, rather than
having code for them generated by the expansion of the defclass form.
That's irrelevant to the two step expansion process though.)


4.  This is a suggestion.  How about defining a set of global variables
which are used in the expansion of DEFCLASS, DEFGENERIC and DEFMETHOD
macros.  These specify the default metaclass to instantiate when creating
classes, slots, generic-functions and methods:

(defvar *default-metaclass* 'standard-class)
(defvar *default-slot-class* 'standard-slot-description)
(defvar *default-generic-function-class* 'standard-generic-function)
(defvar *default-method-class* 'standard-method)

This way the user could bind these variables before loading a set of
definitions to control the class of what is being created without having to
physically modify each DEF<...> in the source files.  For example:

(setf *default-metaclass* 'persistant-class)
(load "my-defclass-file.l")

Of course, if a :METACLASS or :SLOT-CLASS option is supplied it overrides
the default.  These variables are used in macro expanding functions I've
given above.


5.  I am wondering if we should allow generic-functions to contain a
non-homogeneous set of methods.  The :METHOD-CLASS option seems too
restrictive.  I may want to make one particular method in the
generic-function a subclass of the required method-class.  For this reason,
I suggest that DEFMETHOD and DEFGENERIC-OPTIONS both allow a metaclass
option, and :METHOD-CLASS be eliminated from DEFGENERIC-OPTIONS.  For
example:

(defgeneric-options doit (f)
  :documentation "The DOIT generic-function does it."
  :generic-function-class my-special-generic-function)

(defmethod doit ((f foo))
  :method-class broadcast-method
  (print 'hello))

This defines DOIT to be an instance of MY-SPECIAL-GENERIC-FUNCTION (which
is a subclass of STANDARD-GENERIC-FUNCTION), and the FOO DOIT method to be
an instance of the BROADCAST-METHOD class.  The FOO DOIT method should
expand to:

(add-method (find-or-create-generic-function 'doit)
	    (make-instance 'broadcast-method
	      :name 'doit 
	      :lambda-list '((f foo))
	      :body '((print 'hello))))

When no :METHOD-CLASS option is specified the value of
*DEFAULT-METHOD-CLASS* is used.  The body should already be parsed by this
point by EXPAND-DEFMETHOD-BODY.

I would also suggest the addition of EXPAND-DEFGENERIC-OPTIONS to the MOP:

(defmacro defgeneric-options (name lambda-list &rest options)
  (let ((metaclass (class-named (or (getf options :generic-function-class)
				    *default-generic-function-class*))))
    (remf options :generic-function-class)
    (expand-defgeneric-options metaclass name lambda-list options)))

(defmethod expand-defgeneric-options ((c standard-class)
				      name lambda-list options)
  ...)


6.  Nothing seems to be said in the MOP about metaclasses compatibility.  I
would hope that the following code will work:

(defclass my-class (standard-class) ())

(defclass foo () ())
(defclass bar (foo) () :metaclass my-class)

Since nothing is changed from STANDARD-CLASS to MY-CLASS, everything should
be identitical (the way instances are allocated and initialized, the way
defclass is parsed, etc.)  At this point MY-CLASS should be interchangable
with STANDARD-CLASS, and therefore compatible.


7.  Regarding the mixin-base vs. common-base style of defining
standard-class, I just wanted to add a few notes.

The idea of the mixin-base style intimately tying behaviors to classes is
intriguing.  This seems to be moving in the direction of protocol-classes
and classes which implement that protocol.  The protocol-class would simply
define the "external" (meaning required to be supported) operations of the
protocol, and subclasses of the protocol-class would supply specific
methods (and perhaps additional slots) which implement that protocol.  In
this way it is possible to have several implementations of a protocol and
simply use (TYPEP <instance> <protocol-class>) to test whether an object
supports a protocol.  Formally, TYPEP should probably only be used with its
second argument as a protocol-class.  Protocol-classes should only inherit
from other protocol-classes, whereas implementation classes can inherit
from other implementations.  Defining an implementation class only in terms
of a protocol insures that any class that implements that protocol may be
used at runtime, whereas calling methods and accessing slots of another
implementation class not included in a protocol violates encapsulation.
Protocol classes should not be instantiated directly.  

I'm not proposing that protocol classes be added to the language as a first
class feature, but this is exciting because it all seems doable within the
confines of CLOS.  The key question is how to partition the protocols
involved in CLOS into a well defined independent and additive set of
behaviors.  Pierre Cointe's recent paper in OOPSLA-87 may shed some light
on what the minimal requirements for classes and objects are.  Other
protocols are implied by the MOP: namable, forward-referencable, specific
update protocols, specific initialization protocols, etc.

The common-base style certainly eliminates the need to partition protocols,
but has what I think is one serious flaw:  requiring the user to define
protocol predicates.  I have found this to be a major disadvantage in
programming Common Objects which uses this mechanism.  The problem is that
although the predicate can specify whether a protocol is met, it has no
inherent notion of protocol inheritance, and no way of querying an object
as to the protocols it supports.