BetaCLOS

49 views
Skip to first unread message

Pascal Costanza

unread,
Apr 9, 2005, 9:24:08 AM4/9/05
to
Hi,

Goldberg, Findler and Flatt have described an extension of an object
system called BetaJava that combines Java's super and Beta's inner calls
in their paper "Super and Inner - Together at Last!" (OOPSLA'04,
http://www.cs.utah.edu/plt/publications/oopsla04-gff.pdf ), implemented
in MzScheme.

In that paper, the authors make two claims about CLOS in the related
work section:

a) They state that CLOS can simulate inner calls with :around methods,
but that one cannot have multiple inner calls that move down a class
hierarchy, from least specific to most specific class.

b) They also state that CLOS does not allow to have both super and inner
calls within the same method. (In CLOS, the super call is named
call-next-method, an inner call could be named call-inner-method.)

Both claims are wrong.

a) Multiple inner calls can be implemented in "pure" CLOS by way of a
user-defined method combination. Just take the example from the Common
Lisp specification at
http://www.lispworks.com/documentation/HyperSpec/Body/m_defi_4.htm#define-method-combination
that implements the default method-combination technique ('standard),
and reverse the order of the around methods.

b) The BetaJava model can be implemented in about 100 lines of CLOS when
a CLOS MOP implementation is available that conforms to the
specification given in "The Art of the Metaobject Protocol" (Kiczales,
des Rivieres, Bobrow). Here is the code (tested with Steel Bank Common
Lisp 0.8.21):

(use-package :sb-mop)

(defclass beta-generic-function (standard-generic-function)
()
(:metaclass funcallable-standard-class))

(defclass beta-method (standard-method)
((betap :reader betap
:initarg :betap
:initform nil)))

(defmethod initialize-instance :around
((method beta-method)
&rest initargs
&key qualifiers
&allow-other-keys)
(declare (dynamic-extent initargs))
(if (equal qualifiers '(:beta))
(apply #'call-next-method method
:qualifiers ()
:betap t
initargs)
(call-next-method)))

(defun collect-blobs (methods blob blobs)
(cond
((null methods)
(if blob (cons (nreverse blob) blobs) blobs))

((null blob)
(collect-blobs (cdr methods) (list (car methods)) blobs))

((betap (car methods))
(collect-blobs methods () (cons (nreverse blob) blobs)))

(t (collect-blobs (cdr methods) (cons (car methods) blob) blobs))))

(define-method-combination beta ()
((around (:around))
(before (:before))
(primary () :required t)
(after (:after)))
(flet ((call-methods (methods)
(mapcar (lambda (method) `(call-method ,method))
methods)))
(let ((form (if (or before after (rest primary))
(let ((blobs (collect-blobs primary () ())))
`(multiple-value-prog1
(progn ,@(call-methods before)
(call-method ,(first (first blobs))
,(rest (first blobs))
,(rest blobs)))
,@(call-methods (reverse after))))
`(call-method ,(first primary)))))
(if around
`(call-method ,(first around)
(,@(rest around)
(make-method ,form)))
form))))

(defmethod make-method-lambda
((gf beta-generic-function) method-prototype
lambda-expression environment)
(declare (ignore method-prototype environment))
(let ((method-args (gensym))
(next-methods (gensym))
(inner-blobs (gensym)))
`(lambda (,method-args &optional ,next-methods ,inner-blobs)
(declare (ignorable ,next-methods ,inner-blobs))
(flet ((call-next-method (&rest args)
(declare (dynamic-extent args))
(if (null ,next-methods)
(error "There is no next method for ~S." ,gf)
(funcall (method-function (car ,next-methods))
(if args args ,method-args)
(cdr ,next-methods)
,inner-blobs)))
(next-method-p () (not (null ,next-methods)))
(call-inner-method (&rest args)
(declare (dynamic-extent args))
(if (null ,inner-blobs)
(error "There is no inner method for ~S." ,gf)
(funcall (method-function (caar ,inner-blobs))
(if args args ,method-args)
(cdar ,inner-blobs)
(cdr ,inner-blobs))))
(inner-method-p () (not (null ,inner-blobs))))
(apply ,lambda-expression ,method-args)))))

(defmacro define-beta-function (name (&rest args) &rest options)
`(defgeneric ,name ,args
,@(unless (member :generic-function-class options :key #'car)
'((:generic-function-class beta-generic-function)))
,@(unless (member :method-class options :key #'car)
'((:method-class beta-method)))
,@(unless (member :method-combination options :key #'car)
'((:method-combination beta)))
,@options))

Here is an example:

(defclass top () ())
(defclass middle (top) ())
(defclass bottom (middle) ())

(define-beta-function test (object))

(defmethod test ((object top))
(print 'top))

(defmethod test :beta ((object middle))
(print 'middle)
(call-inner-method)
(call-next-method)
(values))

(defmethod test ((object bottom))
(print 'bottom))

(test (make-instance 'bottom))

=>

MIDDLE
BOTTOM
TOP


Something similar should be possible with the Swindle library.

Pascal

--
2nd European Lisp and Scheme Workshop
July 26 - Glasgow, Scotland - co-located with ECOOP 2005
http://lisp-ecoop05.bknr.net/

Bruno Haible

unread,
Apr 12, 2005, 10:46:03 AM4/12/05
to
Pascal Costanza <p...@p-cos.net> posted an example use of the MOP including
MAKE-METHOD-LAMBDA that works fine in interpreted mode:

$ sbcl
(load "betaclos1")
(load "betaclos2")
(load "betaclos3")
(load "betaclos4")


(test (make-instance 'bottom))
=>
MIDDLE
BOTTOM
TOP

(I have split the example into four pieces:
- betaclos1 contains the macros,
- betaclos2 contains the class definitions,
- betaclos3 contains the defgeneric forms,
- betaclos4 contains the defmethod forms.)

But the same example does not work when the compiler is involved:

$ sbcl
(compile-file "betaclos1")
(load "betaclos1")
(compile-file "betaclos2")
(load "betaclos2")
(compile-file "betaclos4")
(compile-file "betaclos3")
(load "betaclos3")
(load "betaclos4")


(test (make-instance 'bottom))
=>
MIDDLE

debugger invoked on a UNDEFINED-FUNCTION in thread 2586:
The function CALL-INNER-METHOD is undefined.

This shows a misdesign of the MAKE-METHOD-LAMBDA generic function: it mixes
compile-time behaviour and run-time behaviour in a broken way.

SBCL behaves as if the DEFGENERIC form must be present at compile time,
before the DEFMETHOD forms can even be compiled. However, this contradicts
ANSI CL.

1) ANSI CL 3.2.2.3 specifies

"Except in the situations explicitly listed above, a function defined
in the evaluation environment is permitted to have a different
definition or a different signature at run time, and the run-time
definition prevails."

This means that a compiler cannot assume that the generic-function class of
a generic function is the same at run-time than at compile-time.

2) ANSI CL DEFGENERIC is specified like this:

"DEFGENERIC is not required to perform any compile-time side effects.
In particular, the methods are not installed for invocation during
compilation. An implementation may choose to store information about
the generic function for the purposes of compile-time error-checking
(such as checking the number of arguments on calls, or noting that
a definition for the function name has been seen)."

This means that the user is expected to be able to compile the DEFGENERIC
form after the DEFMETHOD forms.

Therefore it should be possible to compile betaclos4 with just betaclos1 and
betaclos2 loaded. But as the experience shows, it doesn't work!

Bruno

================================ betaclos1.lisp ================================
(eval-when (load compile eval)
(use-package :sb-mop))

================================ betaclos2.lisp ================================


(defclass top () ())
(defclass middle (top) ())
(defclass bottom (middle) ())

================================ betaclos3.lisp ================================
(define-beta-function test (object))
================================ betaclos4.lisp ================================


(defmethod test ((object top))
(print 'top))

(defmethod test :beta ((object middle))
(print 'middle)
(call-inner-method)
(call-next-method)
(values))

(defmethod test ((object bottom))
(print 'bottom))

================================================================================

Kalle Olavi Niemitalo

unread,
Apr 12, 2005, 3:38:23 PM4/12/05
to
Bruno Haible <br...@clisp.org> writes:

> This shows a misdesign of the MAKE-METHOD-LAMBDA generic function: it mixes
> compile-time behaviour and run-time behaviour in a broken way.
>
> SBCL behaves as if the DEFGENERIC form must be present at compile time,
> before the DEFMETHOD forms can even be compiled. However, this contradicts
> ANSI CL.

If the extended function name is not yet bound to a generic
function when DEFMETHOD is being expanded, then SBCL 0.8.20.5
gives STANDARD-GENERIC-FUNCTION and STANDARD-METHOD to
MAKE-METHOD-LAMBDA by default. I think this means code that uses
only ANSI CL facilities will work, and code that uses the MOP
needs to jump through extra hoops. In my opinion, that is a
reasonable compromise, if it is properly documented.

What is the alternative? I presume there must still be something
akin to MAKE-METHOD-LAMBDA, so that the (call-inner-method) form
can be properly compiled. Currently, MAKE-METHOD-LAMBDA chooses
the processing rules based on the classes of the generic function
and the method. Would you prefer to specify the processing rules
in each DEFMETHOD form instead, so that the class of the generic
function need not be known at compile time? E.g.

(define-beta-method test :beta ((object middle))

Pascal Costanza

unread,
Apr 13, 2005, 5:18:27 AM4/13/05
to
Kalle Olavi Niemitalo wrote:

> Bruno Haible <br...@clisp.org> writes:
>
>>This shows a misdesign of the MAKE-METHOD-LAMBDA generic function: it mixes
>>compile-time behaviour and run-time behaviour in a broken way.
>>
>>SBCL behaves as if the DEFGENERIC form must be present at compile time,
>>before the DEFMETHOD forms can even be compiled. However, this contradicts
>>ANSI CL.
>
> If the extended function name is not yet bound to a generic
> function when DEFMETHOD is being expanded, then SBCL 0.8.20.5
> gives STANDARD-GENERIC-FUNCTION and STANDARD-METHOD to
> MAKE-METHOD-LAMBDA by default. I think this means code that uses
> only ANSI CL facilities will work, and code that uses the MOP
> needs to jump through extra hoops. In my opinion, that is a
> reasonable compromise, if it is properly documented.

...and this is an implication of the AMOP spec as well. defmethod is
specified to expand to code that calls ensure-generic-function, and in
turn ensure-generic-function is specified to generate a generic function
of class standard-generic-function if the requested gf doesn't exist.
(This is actually specified in ensure-generic-function-using-class.)

Furthermore, the AMOP explicitly states that it doesn't attempt to
specify anything about compile-time behavior. (See the section
"Compile-file Processing of the User Interface Macros" in the Introduction.)

The real issue behind this all, AFAICS, is that it is hard / probably
impossible to come up with a sane semantics for change-class on generic
function metaobjects (and other metaobjects as well).

> What is the alternative? I presume there must still be something
> akin to MAKE-METHOD-LAMBDA, so that the (call-inner-method) form
> can be properly compiled. Currently, MAKE-METHOD-LAMBDA chooses
> the processing rules based on the classes of the generic function
> and the method. Would you prefer to specify the processing rules
> in each DEFMETHOD form instead, so that the class of the generic
> function need not be known at compile time? E.g.
>
> (define-beta-method test :beta ((object middle))
> (print 'middle)
> (call-inner-method)
> (call-next-method)
> (values))

This wouldn't be very good because the transition from "pure" CLOS
methods to BetaCLOS methods wouldn't be very smooth. Not that I expect
that lots of people want to use BetaCLOS, but in general you wouldn't
want to introduce new defmethod-style macros for each and every method
class.

However, maybe in this specific example there is indeed no need for the
beta-generic-function class. The two interesting gfs that I have
specialized are initalize-instance on beta-method and make-method-lambda
which could as well be specialized just on its second argument.

But then again, in the general case, you want to subclass
standard-generic-function, and then Bruno's remarks are important.

Maybe we need a forward-referenced-generic-function? ;)

Bruno Haible

unread,
Apr 13, 2005, 9:09:57 AM4/13/05
to
Kalle Olavi Niemitalo wrote:
> If the extended function name is not yet bound to a generic
> function when DEFMETHOD is being expanded, then SBCL 0.8.20.5
> gives STANDARD-GENERIC-FUNCTION and STANDARD-METHOD to
> MAKE-METHOD-LAMBDA by default. I think this means code that uses
> only ANSI CL facilities will work, and code that uses the MOP
> needs to jump through extra hoops.

These "extra hoops" are a change in development methodology: In
ANSI CL, DEFMETHOD forms are independent of DEFGENERIC forms.
The programmer knows that he can compile and load his files in
arbitrary order, as long as macros and classes are defined early.
When he uses MAKE-METHOD-LAMBDA, generic function declarations
suddenly have to be defined early as well.

> What is the alternative? I presume there must still be something
> akin to MAKE-METHOD-LAMBDA, so that the (call-inner-method) form
> can be properly compiled. Currently, MAKE-METHOD-LAMBDA chooses
> the processing rules based on the classes of the generic function
> and the method. Would you prefer to specify the processing rules
> in each DEFMETHOD form instead, so that the class of the generic
> function need not be known at compile time? E.g.
>
> (define-beta-method test :beta ((object middle))
> (print 'middle)
> (call-inner-method)
> (call-next-method)
> (values))

You don't need 'define-beta-method': The programmer has already
specified the type of method when he wrote

(defmethod test :beta ((object middle))


(print 'middle)
(call-inner-method)
(call-next-method)
(values))

A replacement for this ill-designed MAKE-METHOD-LAMBDA would be to
specify the effect of the qualifier :BETA on the macroexpansion.
Something like

(defgeneric augment-method-lambda (qualifier lambdabody)
(:method (qualifier lambdabody) lambdabody))

(defmethod augment-method-lambda ((qualifier (eql :beta)) lambdabody)
(let ((lambdalist (car lambdabody))
(body (cdr lambdabody)))
(cons lambdalist
`(let ((inner-methods *inner-methods*))
(flet ((call-inner-method () ...[access inner-methods]...))
,@body)))))

This method for augment-method-lambda on (EQL :BETA) would be required
to be known at compile-time, but the generic function declaration of
TEST would not.

Bruno

Bruno Haible

unread,
Apr 13, 2005, 9:22:30 AM4/13/05
to
Pascal Costanza wrote:

> Furthermore, the AMOP explicitly states that it doesn't attempt to
> specify anything about compile-time behavior.

Yes. The MOP authors haven't thought about it. For most of the MOP,
there is no problem with compile-time behaviour, but regarding
MAKE-METHOD-LAMBDA there is a big problem.

> this is an implication of the AMOP spec as well. defmethod is
> specified to expand to code that calls ensure-generic-function, and in
> turn ensure-generic-function is specified to generate a generic function
> of class standard-generic-function if the requested gf doesn't exist.
> (This is actually specified in ensure-generic-function-using-class.)

These are unrelated issues. DEFMETHOD's behaviour at EVAL or LOAD time
is well defined in ANSI CL and the MOP. The issue is whether when you
compile a DEFMETHOD form, it depends on the DEFGENERIC already not only
having been _compiled_earlier_, but even having been _loaded_earlier_
in the compilation environment.

> The real issue behind this all, AFAICS, is that it is hard / probably
> impossible to come up with a sane semantics for change-class on generic
> function metaobjects (and other metaobjects as well).

This is unrelated as well. By the way, GNU clisp 2.33.80 supports
CHANGE-CLASS on generic functions. If you find a semantic problem with
it, please let me know :-)

Bruno

Pascal Costanza

unread,
Apr 13, 2005, 10:52:09 AM4/13/05
to
Bruno Haible wrote:

> Pascal Costanza wrote:
>
>>Furthermore, the AMOP explicitly states that it doesn't attempt to
>>specify anything about compile-time behavior.
>
> Yes. The MOP authors haven't thought about it. For most of the MOP,
> there is no problem with compile-time behaviour, but regarding
> MAKE-METHOD-LAMBDA there is a big problem.
>
>>this is an implication of the AMOP spec as well. defmethod is
>>specified to expand to code that calls ensure-generic-function, and in
>>turn ensure-generic-function is specified to generate a generic function
>>of class standard-generic-function if the requested gf doesn't exist.
>>(This is actually specified in ensure-generic-function-using-class.)
>
> These are unrelated issues. DEFMETHOD's behaviour at EVAL or LOAD time
> is well defined in ANSI CL and the MOP. The issue is whether when you
> compile a DEFMETHOD form, it depends on the DEFGENERIC already not only
> having been _compiled_earlier_, but even having been _loaded_earlier_
> in the compilation environment.

OK.

>>The real issue behind this all, AFAICS, is that it is hard / probably
>>impossible to come up with a sane semantics for change-class on generic
>>function metaobjects (and other metaobjects as well).
>
> This is unrelated as well. By the way, GNU clisp 2.33.80 supports
> CHANGE-CLASS on generic functions. If you find a semantic problem with
> it, please let me know :-)

I think it is. If a generic function is created at compile-time, and
subsequently a defgeneric form is loaded that specifies a different
generic function class, a change-class has to be issued. (Or I am
missing something here?!?)

Change-class on metaobjects imposes semantic problems that go beyond
whether you can actually implement change-class or not. What should
happen to a generic function whose purpose is to make remote calls when
you suddenly change it to some other generic function class that
doesn't? There's no general solution here that is as convenient as the
notion of obsolete instances of plain CLOS.

Kalle Olavi Niemitalo

unread,
Apr 13, 2005, 10:52:05 AM4/13/05
to
Bruno Haible <br...@clisp.org> writes:

> A replacement for this ill-designed MAKE-METHOD-LAMBDA would be to
> specify the effect of the qualifier :BETA on the macroexpansion.

I suppose you could do that. To avoid conflicts between
programs, the qualifiers would better not be keywords.
Would the qualifiers used in this process still be visible
to method combination types?

Pascal Costanza

unread,
Apr 13, 2005, 11:49:16 AM4/13/05
to
Kalle Olavi Niemitalo wrote:

This would get confusing, I think, because qualifiers are definitely
meant for method combinations and shouldn't be mixed with other things.

I am currently working on an extension of the defmethod form that allows
method options to be specified. For example, you could say this:

(defmethod test :beta ((object middle))

(:method-class beta-method)
...)

It allows more options there, like:

(defmethod test2 ((object middle))
(:method-class gamma-method)
(:remote t)
...)

The additional options are just passed to make-instance of the method
class. (Predefined method options, like :qualifiers, :lambda-list, etc.,
are rejected.)

I have used the :beta combination as a hack to get what I want, but I
would actually prefer to just say:

(defmethod test ((object middle))
(:method-class beta-method)
...)

...and then replace (betap ...) with (typep ... 'beta-method) in my
code. The default method class for a beta-generic-function would then
stay standard-method.


This way of declaring method options doesn't interfere with the existing
defmethod form because forms with keywords as the car cannot be possibly
executed, and defmethod already recognizes documentation strings and
declarations as "non-code".

Bruno Haible

unread,
Apr 13, 2005, 2:50:39 PM4/13/05
to
Kalle Olavi Niemitalo wrote:
>> A replacement for this ill-designed MAKE-METHOD-LAMBDA would be to
>> specify the effect of the qualifier :BETA on the macroexpansion.
>
> To avoid conflicts between programs, the qualifiers would better not
> be keywords. Would the qualifiers used in this process still be
> visible to method combination types?

Yes. The DEFMETHOD expansion (produced at compile-time) and the
method-combination (computed at runtime for a given set of applicable
methods) are computed at different times. But the method-combination
must handle each kind of method specially: The :BETA methods, for
example, need to access the list of remaining inner methods. It's the
method-combination which must provide this info. So, the qualifiers of
the methods must be used in both places.

Bruno

Kalle Olavi Niemitalo

unread,
Apr 13, 2005, 2:51:29 PM4/13/05
to
Pascal Costanza <p...@p-cos.net> writes:

> This way of declaring method options doesn't interfere with the
> existing defmethod form because forms with keywords as the car cannot
> be possibly executed, and defmethod already recognizes documentation
> strings and declarations as "non-code".

If you used (declare (method-option (:method-class beta-method))),
then you could also reliably use option symbols that are not keywords.
It'd be more verbose, though.

Bruno Haible

unread,
Apr 13, 2005, 3:01:04 PM4/13/05
to
Pascal Costanza wrote:
> If a generic function is created at compile-time, and
> subsequently a defgeneric form is loaded that specifies a different
> generic function class, a change-class has to be issued.

Yes.

> What should
> happen to a generic function whose purpose is to make remote calls when
> you suddenly change it to some other generic function class that doesn't?

Well, then it will not do remote calls any more :-) Where is the semantic
problem?

There is no semantic problem with changing the behaviour of a function
through adding or removing methods from it, or through modification of
its slots' values. Why then should CHANGE-CLASS be different?

Bruno

Pascal Costanza

unread,
Apr 14, 2005, 5:44:04 AM4/14/05
to
Bruno Haible wrote:

> Pascal Costanza wrote:
>
>>If a generic function is created at compile-time, and
>>subsequently a defgeneric form is loaded that specifies a different
>>generic function class, a change-class has to be issued.
>
> Yes.
>
>>What should
>>happen to a generic function whose purpose is to make remote calls when
>>you suddenly change it to some other generic function class that doesn't?
>
> Well, then it will not do remote calls any more :-) Where is the semantic
> problem?

That's the semantic problem. ;)

> There is no semantic problem with changing the behaviour of a function
> through adding or removing methods from it, or through modification of
> its slots' values. Why then should CHANGE-CLASS be different?

You use a generic function class for a reason. When changing a gf's
class means that you may lose all its important properties, you could as
well start with a fresh one.

On the base level, change-class gives you a semantics-preserving
operation: All the slots with the same names are preserved and you can
fine-tune what happens with the rest. For metaobject classes, the slot
names are not even specified.

Even if they were specified, you get a problem: The interesting thing
about, say, a remote generic function is that invoking it gives you the
behavior of the remote operation. I would expect that changing its class
preserves that behavior. Here, I don't mean the technical details of
remote function invocation, but what the remote operation actually does.

It may be possible to come up with some semantics for a change from
standard-generic-function to one's own generic function class, because
the semantics of standard-generic-function are well know. But things
become really tough if you want to change from some arbitary subclass of
generic-function to some other arbitrary sublass of generic-function. I
don't think that the update-instance-for-different-class protocol is
sufficient to handle such changes.

Pascal Costanza

unread,
Apr 14, 2005, 5:46:46 AM4/14/05
to
Kalle Olavi Niemitalo wrote:

...and wouldn't fit well with the rest of CLOS. Both defclass and
defgeneric allow you to specify options as part of the "top-most level"
of those operators. Furthermore in the case of defgeneric, declarations
and generic function class options are kept separately. I think it's
better to keep that "spirit".

Bruno Haible

unread,
Apr 14, 2005, 12:48:18 PM4/14/05
to
Pascal Costanza wrote:
>
> (defmethod test2 ((object middle))
> (:method-class gamma-method)
> (:remote t)
> ...)
>
> This way of declaring method options doesn't interfere with the existing
> defmethod form because forms with keywords as the car cannot be possibly
> executed

However, this doesn't fit well with the rest of ANSI CL. In ANSI CL,
anyplace where keywords are allowed, normal symbols are allowed as well.
The idea is that normal symbols are used to designate things in a
namespace limited to the package, whereas keywords are used when a
global namespace is desired. If the method-class FOO:GAMMA-METHOD
wants to avoid a clash with the method-class BAR:PROXY-METHOD
on the meaning of "remote", one will use FOO:REMOTE as marker, and
the other one will use BAR:REMOTE.

So the syntax

(defmethod test2 ((object middle))
(:method-class gamma-method)

(foo:remote t)
...)

will not work. You'll need a different syntax, like

(defmethod test2 ((object middle))
(:method-class gamma-method)

(:method-options foo:remote t)
...)

or

(defmethod test2 ((object middle))
(:method-class gamma-method foo:remote t)
...)


Bruno

Bruno Haible

unread,
Apr 14, 2005, 1:11:31 PM4/14/05
to
Pascal Costanza wrote:
>
> The interesting thing
> about, say, a remote generic function is that invoking it gives you the
> behavior of the remote operation. I would expect that changing its class
> preserves that behavior.

That's a general problem of CHANGE-CLASS on any kind of object. All
kinds of objects have behaviour. You can also call it the "contract"
of an object.

For example, if I have an object of type TRIANGLE and change it to
become an object of type SQUARE, its contract also changes in ways
that poses semantic problems. Namely, I could have a method
(defmethod compute-vertices ((x triangle) midpoint) ...)
and a method
(defmethod compute-vertices ((x square) midpoint) ...)
The contract of TRIANGLE implies that compute-vertices of it returns a
list of 3 points. After it's been changed to be a SQUARE, this
contract does not hold any more.

The only case where CHANGE-CLASS does not exhibit this kind of
semantic problem is when the new object's class is the same or a
subclass of the old object's class.

Bruno

Pascal Costanza

unread,
Apr 14, 2005, 1:23:22 PM4/14/05
to

OK, indeed. The :method-options variant looks good to me.

Pascal Costanza

unread,
Apr 14, 2005, 1:33:32 PM4/14/05
to
Bruno Haible wrote:
> Pascal Costanza wrote:
>
>>The interesting thing
>>about, say, a remote generic function is that invoking it gives you the
>>behavior of the remote operation. I would expect that changing its class
>>preserves that behavior.
>
> That's a general problem of CHANGE-CLASS on any kind of object. All
> kinds of objects have behaviour. You can also call it the "contract"
> of an object.
>
> For example, if I have an object of type TRIANGLE and change it to
> become an object of type SQUARE, its contract also changes in ways
> that poses semantic problems. Namely, I could have a method
> (defmethod compute-vertices ((x triangle) midpoint) ...)
> and a method
> (defmethod compute-vertices ((x square) midpoint) ...)
> The contract of TRIANGLE implies that compute-vertices of it returns a
> list of 3 points. After it's been changed to be a SQUARE, this
> contract does not hold any more.

Yes, but still the slots that both classes have in common are preserved,
and other functions may still be applicable to the object after its
class has been changed. So there is a chance that change-class can be
useful at the base level. (Although change-class may have problems even
there, but if used with caution it can be useful.)

I have problems imagining a useful application of being able to change
the class of a generic function.

> The only case where CHANGE-CLASS does not exhibit this kind of
> semantic problem is when the new object's class is the same or a
> subclass of the old object's class.

So what happens to the primary methods of a standard-generic-function
when you change it to a remote-generic-function, assuming that the
latter is a subclass of the former?

Bruno Haible

unread,
Apr 14, 2005, 2:02:54 PM4/14/05
to
Pascal Costanza wrote:
>
> I have problems imagining a useful application of being able to change
> the class of a generic function.

The following can make use of it:
- Tracing generic functions without changing their identity.
- Profiling generic functions without changing their identity.
- Debuggable generic functions: add REDO and RETURN restarts to
each method-invocation in the effective-method.
- Temporarily filtering out some applicable methods (with a method on
COMPUTE-APPLICABLE-METHODS and COMPUTE-APPLICABLE-METHODS-USING-CLASSES).
- Aspect-oriented programming?

>> The only case where CHANGE-CLASS does not exhibit this kind of
>> semantic problem is when the new object's class is the same or a
>> subclass of the old object's class.
>
> So what happens to the primary methods of a standard-generic-function
> when you change it to a remote-generic-function, assuming that the
> latter is a subclass of the former?

It will hopefully leave the methods in place. If your implementation
of remote-generic-function ignores the primary methods (for example,
because it is implemented through method combination or similar),
that's fine. Otherwise you may have to write a method for U-I-F-D-C
that will remove the primary methods and replace them with a single
method that does the indirection.

Bruno

Pascal Costanza

unread,
Apr 14, 2005, 3:39:02 PM4/14/05
to
Bruno Haible wrote:

> Pascal Costanza wrote:
>
>>I have problems imagining a useful application of being able to change
>>the class of a generic function.
>
> The following can make use of it:
> - Tracing generic functions without changing their identity.
> - Profiling generic functions without changing their identity.
> - Debuggable generic functions: add REDO and RETURN restarts to
> each method-invocation in the effective-method.
> - Temporarily filtering out some applicable methods (with a method on
> COMPUTE-APPLICABLE-METHODS and COMPUTE-APPLICABLE-METHODS-USING-CLASSES).

Nah, those are too obvious. ;-)

> - Aspect-oriented programming?

Hmmm... ;)

OK, I stand corrected.

Pascal Costanza

unread,
Apr 16, 2005, 9:00:14 AM4/16/05
to
Bruno Haible wrote:

> You don't need 'define-beta-method': The programmer has already
> specified the type of method when he wrote
>
> (defmethod test :beta ((object middle))
> (print 'middle)
> (call-inner-method)
> (call-next-method)
> (values))
>
> A replacement for this ill-designed MAKE-METHOD-LAMBDA would be to
> specify the effect of the qualifier :BETA on the macroexpansion.
> Something like
>
> (defgeneric augment-method-lambda (qualifier lambdabody)
> (:method (qualifier lambdabody) lambdabody))
>
> (defmethod augment-method-lambda ((qualifier (eql :beta)) lambdabody)
> (let ((lambdalist (car lambdabody))
> (body (cdr lambdabody)))
> (cons lambdalist
> `(let ((inner-methods *inner-methods*))
> (flet ((call-inner-method () ...[access inner-methods]...))
> ,@body)))))
>
> This method for augment-method-lambda on (EQL :BETA) would be required
> to be known at compile-time, but the generic function declaration of
> TEST would not.

I don't think that's a good idea:

- The :beta qualifier was a hack to get the behavior I was interested
in. My first attempt was to have a separate method class for beta
methods, and I think that's a cleaner design.

- You may have more than one qualifier. The AMOP allows for a list of
qualifiers, and it's harder to design such a kind of
augment-method-lambda to work on multiple qualifiers. (I have an example
where I actually need multiple qualifiers, and I don't consider it a toy
example.)

- A subclass of beta-generic-function may want to do more than "just"
insert a call-inner-method function. For example, it may want to combine
beta methods with some other kinds of methods from another generic
function class. So the AMOP is right that the method lambda should
depend on the generic function class.

I agree with Kalle that it's ok that a CLOS implementation can deviate
from the ANSI standard when it handles subclasses of the standard
metaobject classes.

One possibility would be to define defgeneric like this:

(defmacro defgeneric (name (&rest args) &body options)
(let ((class-option (find :generic-function-class
options
:key #'car)))
(if (and class-option
(not (eq (cadr class-option) 'standard-generic-function)))
`(really-defgeneric ,name ,args ,@options)
`(eval-when (:compile-toplevel :load-toplevel :execute)
(really-defgeneric ,name ,args ,@options)))))

...and then change the AMOP spec to explicitly require gfs to be
globally declared when they are supposed to be something other than
standard-generic-function.

Bruno Haible

unread,
Apr 18, 2005, 2:03:38 PM4/18/05
to
Pascal Costanza wrote:
>>
>> (defgeneric augment-method-lambda (qualifier lambdabody)
>> (:method (qualifier lambdabody) lambdabody))
>>
>> (defmethod augment-method-lambda ((qualifier (eql :beta)) lambdabody)
>> (let ((lambdalist (car lambdabody))
>> (body (cdr lambdabody)))
>> (cons lambdalist
>> `(let ((inner-methods *inner-methods*))
>> (flet ((call-inner-method () ...[access inner-methods]...))
>> ,@body)))))
>>
>> This method for augment-method-lambda on (EQL :BETA) would be required
>> to be known at compile-time, but the generic function declaration of
>> TEST would not.
>
> I don't think that's a good idea:
>
> - You may have more than one qualifier. The AMOP allows for a list of
> qualifiers, and it's harder to design such a kind of
> augment-method-lambda to work on multiple qualifiers.

When you have more then one qualifier, augment-method-lambda can be
applied once for every qualifier, in turn.

> - A subclass of beta-generic-function may want to do more than "just"
> insert a call-inner-method function. For example, it may want to combine
> beta methods with some other kinds of methods from another generic
> function class.

Such functionality should be implemented in a way that fits in a world with
a compiler, and with the possibility to use ADD-METHOD and REMOVE-METHOD.
This means, DEFMETHOD is meant to generate the code at compile time once,
without knowledge of the generic function class - that's what ANSI CL says.

> So the AMOP is right that the method lambda should depend on the generic
> function class.

I disagree 100%.

> One possibility would be to define defgeneric like this:
>
> (defmacro defgeneric (name (&rest args) &body options)
> (let ((class-option (find :generic-function-class
> options
> :key #'car)))
> (if (and class-option
> (not (eq (cadr class-option) 'standard-generic-function)))
> `(really-defgeneric ,name ,args ,@options)
> `(eval-when (:compile-toplevel :load-toplevel :execute)
> (really-defgeneric ,name ,args ,@options)))))
>
> ...and then change the AMOP spec to explicitly require gfs to be
> globally declared when they are supposed to be something other than
> standard-generic-function.

And what does this ultimately mean for the programmer in his
programming environment? More compile-time dependencies. More frequent
recompilations of source files that define methods.

Bruno

Reply all
Reply to author
Forward
0 new messages