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

CLOS-style method combination in Self

This is a little long, beware.  Hope it's worth it.  Tell me if not.

CLOS-style method combination in Self

CLOS and BETA both provide facilities for combining pieces of code in
an automated way.  The standard facilities are used for inheriting
code from less specific methods resp. patterns.  In CLOS this can be
modified, I don't know that much about BETA.

The trace of execution will run like this in CLOS:
        _       _        ___        _       _
      _| |    _| | (   _|   |_   ) | |_    | |_       ^
    _|   |  _|   | ( _|	      |_ ) |   |_  |   |_     | (next-method)
  _|     |_|     |_(|           |)_|     |_|	 |_   |

  around  before      primaries     after   around

Whereas in BETA it will run like this:

   	  _        _             _        _
  	   |_    (| |_         _| |)    _|      |
  	     |_  (|   |_     _|   |)  _|        | INNER
  	       |_(|     |___|     |)_|          V

The lower side of the pictures has the more specific stuff.  The
parenthesized part is optional.  In CLOS, it corresponds to calling
(next-method) from the primary method, in BETA, it corresponds to
calling a whole new method.

I don't quite see why the CLOS designers did it this way, I like the
BETA way much better.  There's a reason why with C++ the basic
constructors of any superclasses are called before the more specific
ones.  Sure, one can give the `:most-specific-last' keyword to make it
work the other way, but why isn't it the default?  But this ought to
be the wrong place to ask this anyway.

The scheme I have in mind corresponds to an interpreter.  It should
work, but isn't overly clever.  The Self compiler optimizes
send-sites, it doesn't do anything to remember the result of a lookup
for different senders (or does it?), so the effective method is
computed anew for every new sender.

For a scheme like this to be real-life-proof, it has to be fast.
A better way to do this would be to first construct the code sequence
alias effective method and cache that.  See Common Lisp the Language
2nd ed. p.795, note on method implementation.

Let's make a method called `foo' that consists of parts defined at
different levels of ancestry (english!?), in the object `child' and in
`parent', which is a parent of `child'.

The methods will know some places (slots) that contain the code of the
various `before', `after' and `main' components (`main' being
analogous to CLOS' `primary', which is too awkward).  The method's
code consists mainly of a call to `run'.

Anything that would be put in an around-method in CLOS will go in the
actual code of the foo method here, around (!) the `run' send.  The
`run' method, to be found in some traits object, will then take care
of calling the before-, main- and after-components in the right order.

It's easy.  At least that's what I thought. :-( Then it turned out I have
far too little Self experience to get it to work in just a few hours.  But
it was worth it, I think I learned something.  My Self intuition doesn't
match the real behavior of Self, yet.  (I consider this a virtue ;-)

The various obstacles I stumbled over include:

My first attempt was to put the data associated with this kind of
behavior directly in the affected methods.

(Feel free to skip the code while reading.  Since we don't have
hypertext mail yet, I prefer to keep it here.)

( traits _AddSlotsIfAbsent: (|

  "This code was retyped from memory and is possibly not fully identical"
  "to what I actually tried."

  methodCombinationSentinel = (|
       before = nil.
       after = nil.

  methodCombination = (|
    main = thisMain value. "Default is to just call the most specific"
			    "`thisMain' method.  Call less specific `main'"
			    "methods from there!"
    run = (| result |
	     result: main.
	     result ).

  closMethodCombination = (|
     "CLOS-style is most specific before-methods first, most specific"
     "after-methods last.  `Most specific behavior on the Outside'."
     parent* = traits methodCombination.
     before = ( thisBefore value.  outer before ).
     after = ( outer after.  thisAfter value ).

  betaMethodCombination = (|
     "BETA-style is least specific before-methods first, least specific"
     "after-methods last.  `Least specific behavior on the Outside'."
     parent* = traits methodCombination.
     before = ( outer before.  thisBefore value ).
     after = ( thisAfter value.  outer after ).

(shell _AddSlotsIfAbsent: (|

  parent = (|
     foo = (| traits* = traits closMethodCombination.
      	      outer = traits methodCombinationSentinel.
	      thisBefore = [' going.  ' print]
      	      thisMain = [' and a shove!  ' print].
      	      thisAfter = [' Almost ... ' print].
  child = (|
     parent* = parent.  "This may work or not, don't bother, you know"
			"what I mean.  Drop arrowheads in the UI instead."
     foo = (| traits* = traits closMethodCombination.
	      outer = reflect: parent at: 'foo'.
	      thisBefore = ['Let`s go ... ' print].
	      thisMain = [ 'A push ' print.  outer main. ].
	      thisAfter = [' finished.  ' print].

I tried to make a parent method known in a child method by putting the
parent method in the `outer' slot in the child method.  (Note that the
parent method is not a parent of the child method!)  Evaluating code
to this end would produce a "sorry, local methods not implemented".
OK.  I knew that, that's why I used blocks, not real methods to store
the behavior.  Because I never intended to call it, just to use it as a
context, I hadn't realized that I was still trying to create a local
method, `outer'.  (And yes, these non-LIFO blocks don't work as well.)

In the UI, I couldn't resist and try to drop an arrowhead on the
method object.  The result was some "type error", but the arrowhead
stuck to the method nonetheless.  I didn't bother to check whether
this was just a UI artifact or truly reflected what was going on.  So
it cannot be done this way.  (And, incidentally, cannot even be saved,
since the transporter does not handle non-argument slots in methods.
This also applies to my next try.)

Are there any specific reasons other than lack of time why local
methods were not implemented, like implementation difficulties?  I
don't recall having seen anything written on this.  Could anybody of
the Self group relate to this?

At first I thought that it would be too hard to pull this off without
local methods, but then I realized that I shouldn't have put the data
in the method object in the first place, it belongs in a parent, of
course.  Thanks, Ian, for pointing out that this is possible at all!
If this was mentioned anywhere in the documentation, I missed it.  So
not having directly local methods is no problem here, it's even
sensible to not use them in such contexts.

Putting parent slots in methods seems very interesting indeed, I'd
like to learn of enlightening examples of its use.  Are there any to
be found in the Self system?  Ian, how did you use them?

So I moved the data (which is common to all activations of a method)
to a data-parent, `local*'.  The `outer' slot of a child method would
then point to the `local*' object in the parent method.

Well, does this work?  No, at least not yet.  When calling a method
located in some parent slot of a method, the self*-slot in the called
method is not bound to the calling method but to the contents of its
self*-slot.  `run' will be found, but will then send `before' to the
initial receiver, which will fail.

So foo must not send `run' to implicit self but to a suitable context
object, so that `self' is rebound to that.  This is done by
making the `local' slot a nonparent slot and sending `run' to it.
What about `self' now?  Keeping a link to the receiver of `foo' is no
problem, one can make it a dynamic parent of `local'.  And implicit
self?  How to pass arguments?  Making the activation of `foo' the
dynamic parent of `local' would do it, but how?

This is what the code looks like now:

(shell _AddSlotsIfAbsent: (|

  parent = (|
    foo = (|
      local = (|
	      myself* <- nil.  "See comment for shell child"
	      traits* = traits closMethodCombination.
      	      outer = methodCombinationSentinel.
	      thisBefore = (' going.  ' print)
      	      thisMain = (' and a shove!  ' print).
      	      thisAfter = (' Almost ... ' print).
      local myself: self.
      local run.
  child = (|
    parent* = parent.  "This may work or not, don't bother, you know"
		       "what I mean.  Drop arrowheads in the UI instead."
    foo = (|
      local = (|
      	      myself* <- nil.  "Receiver of `foo' will go here. "
	      "Since this value may be different for different
	      activations, this is very likely to break.  Should at
	      least save/restore it in another local slot of `foo'.
	      Might still break.  So this is not the way to do it."
              traits* = traits closMethodCombination.
	      outer = reflect: reflect: shell parent at: 'foo' at: 'local'.
	      "Ouch, too much reflection.  Dunno if this works, I just"
	      "grabbed the pointer. "

	      thisBefore = ('Let`s go ... ' print).
	      thisMain = ( 'A push ' print.  outer main. ).
	      thisAfter = (' finished.  ' print).
      local myself: self.  "should 
      local run.

If there's interest, I can post the module file.

By the way, the output of the transporter looks real nasty.  Can
this be done in a more (human-)readable way, so as to produce files
that can be read offline (and are more compact)?

All this looks like a kludge.  Before saving the module, one has to
manually move the `local' slots to the method holders, then set their
creators again.  After reading it in, the slots have to be moved back
into the methods.  It is brittle because of bad hygiene.  Some
selectors become reserved.  All sorts of scoping problems will sure
jump on one from around the corner.  And I almost ignored the issue of
passing arguments.  The `run' method must be inlined in `foo' to get

Does anybody have an idea how to do this more elegantly?

My _feeling_ is that there should be an easy way to let slots decide
whether to run the contained object or just return it.  Go ahead, flame me. ;)