Can I get the behavior of `overment` followed by `augride` with a single class?

125 views
Skip to first unread message

Alexis King

unread,
Jul 17, 2021, 1:10:24 AM7/17/21
to Racket Users

Hello,

While doing some OOP in Racket today, I found myself in a situation that would benefit from two seemingly contradictory things:

  1. I want to be able to override a superclass method, and I want to be certain that I get to handle the method before any of my subclasses do. This suggests I want to use inner.

  2. At the same time, I want my subclasses to be able to override this method, not augment it. If I call inner and my subclass calls super, control should jump to my superclass.

In other words, I want to get a sort of “first try” at handling the method so that if I choose to, I can neglect to call my subclass’s implementation altogether. But if I decide not to handle it, then I want super-style dispatch to proceed as if my class were never there at all.

At first, I thought this wasn’t possible using Racket’s class system, since if I override my superclass’s method using overment, the subclass necessarily cannot use super, violating requirement 2. Yet if I use override, I don’t get the “first try” I want, violating requirement 1. However, after some thought, I realized it’s possible if I’m willing to use two classes rather than one:

(define my-superclass%
  (class object%
    (define/public (m x) `(foo ,x))
    (super-new)))

(define my-class%
  (let ()
    (define-local-member-name super-m)
    (class (class my-superclass%
             (define/public (super-m x)
               (super m x))
             (define/overment (m x)
               (if (not x)
                   'skip
                   (inner (error "impossible") m x)))
             (super-new))
      (inherit super-m)
      (define/augride (m x)
        (super-m x))
      (super-new))))

The trick here is twofold:

  1. First, I override m using overment, which ensures method dispatch will call my implementation first.

  2. Next, I augment my own implementation of m using augride, which makes the method overridable again in subclasses. To satisfy the other half of requirement 2, my augmentation calls my-superclass%’s implementation of m via a sort of secret “side channel,” kept private using define-local-member-name.

Using this trick, subclasses of my-class% can still override m, and as long as x is non-#f, my sneaky interposition doesn’t seem to have any effect. But if x is #f, I can short-circuit the computation immediately:

(define my-subclass%
  (class my-class%
    (define/override (m x)
      `(baz ,(super m x)))
    (super-new)))

(define obj (new my-subclass%))
(send obj m #t) ; => '(baz (foo #t))
(send obj m #f) ; => 'skip

I think this is kind of cute, since it makes it possible to effectively conditionally interpose on method dispatch. However, it’s rather awkward to write. This brings me to my question: is there any simpler way to do this? And are there any hidden gotchas to my technique?

Thanks,
Alexis

George Neuner

unread,
Jul 17, 2021, 3:34:52 AM7/17/21
to Alexis King, racket users

On 7/17/2021 1:10 AM, Alexis King wrote:
   :
  a complex, possibly error-prone, way to front-end class method dispatch
   :

> This brings me to my question: is there any simpler way to do this?
> And are there any hidden gotchas to my technique?

I'm still trying to understand how it works.  8-)


However, it occurs to me that, in Lisp, using defgeneric with
/method-combination :most-specific-last/  solves the problem quite
nicely.  Eli Barzilay's old Swindle package still is available ... I
know it had generic methods, but I don't recall whether it implemented
method combination.

I also recall some years back that you wrote about using racket/generic 
and created a simple multiple dispatch system. There doesn't seem to
whole be a lot of documentation regarding generics (other than as
applied to interfaces), so I'm fuzzy on what they can / can't do.


Anyway I doubt Lisp-like generic methods are what you want (else you
wouldn't have started with classes), but it seems that you are trying to
achieve similar functionality.

George

Matthew Flatt

unread,
Jul 19, 2021, 12:08:32 PM7/19/21
to Alexis King, Racket Users
I agree that this doesn't look possible within a single class, but I
don't see any problem with the pattern (aside from being awkward to
write). As far as I can tell, it would make sense for `class` to
support new a case that fuses your `overment` plus `augride` steps.
Internally, that would create a new index for the method while wiring
`super` in subclasses directly to the superclass implementation.

Are there other useful variants that are not currently supported (at
least directly)? Why wasn't this one a blank space in the table of
possibilities that we sketched in the super+inner paper? I don't know.

At Sat, 17 Jul 2021 00:10:10 -0500, Alexis King wrote:
> Hello,
>
> While doing some OOP in Racket today, I found myself in a situation that
> would benefit from two seemingly contradictory things:
>
> 1.
>
> I want to be able to override a superclass method, and I want to be
> certain that I get to handle the method before any of my subclasses do.
> This suggests I want to use inner.
> 2.
>
> At the same time, I want my subclasses to be able to override this
> method, not augment it. If I call inner and my subclass calls super,
> control should jump to *my superclass*.
>
> In other words, I want to get a sort of “first try” at handling the method
> so that if I choose to, I can neglect to call my subclass’s implementation
> altogether. But if I decide *not* to handle it, then I want super-style
> dispatch to proceed as if my class were never there at all.
>
> At first, I thought this wasn’t possible using Racket’s class system, since
> if I override my superclass’s method using overment, the subclass
> necessarily cannot use super, violating requirement 2. Yet if I use override,
> I don’t get the “first try” I want, violating requirement 1. However, after
> some thought, I realized it’s possible if I’m willing to use *two* classes
> rather than one:
>
> (define my-superclass%
> (class object%
> (define/public (m x) `(foo ,x))
> (super-new)))
>
> (define my-class%
> (let ()
> (define-local-member-name super-m)
> (class (class my-superclass%
> (define/public (super-m x)
> (super m x))
> (define/overment (m x)
> (if (not x)
> 'skip
> (inner (error "impossible") m x)))
> (super-new))
> (inherit super-m)
> (define/augride (m x)
> (super-m x))
> (super-new))))
>
> The trick here is twofold:
>
> 1.
>
> First, I override m using overment, which ensures method dispatch will
> call my implementation first.
> 2.
>
> Next, I augment my own implementation of m using augride, which makes
> the method overridable again in subclasses. To satisfy the other half of
> requirement 2, my augmentation calls my-superclass%’s implementation of m
> via a sort of secret “side channel,” kept private using
> define-local-member-name.
>
> Using this trick, subclasses of my-class% can still override m, and as long
> as x is non-#f, my sneaky interposition doesn’t seem to have any effect.
> But if x *is* #f, I can short-circuit the computation immediately:
>
> (define my-subclass%
> (class my-class%
> (define/override (m x)
> `(baz ,(super m x)))
> (super-new)))
>
> (define obj (new my-subclass%))
> (send obj m #t) ; => '(baz (foo #t))
> (send obj m #f) ; => 'skip
>
> I think this is kind of cute, since it makes it possible to effectively
> conditionally interpose on method dispatch. However, it’s rather awkward to
> write. This brings me to my question: is there any simpler way to do this?
> And are there any hidden gotchas to my technique?
>
> Thanks,
> Alexis
>
> --
> You received this message because you are subscribed to the Google Groups
> "Racket Users" group.
> To unsubscribe from this group and stop receiving emails from it, send an email
> to racket-users...@googlegroups.com.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/racket-users/CAA8dsae1HR2CVUNq3aC4nc2vpPU6L9f-
> ENor-074LyZnWXgKYg%40mail.gmail.com.

Alexis King

unread,
Jul 25, 2021, 4:20:04 PM7/25/21
to Matthew Flatt, Racket Users
On Mon, Jul 19, 2021 at 11:08 AM Matthew Flatt <mfl...@cs.utah.edu> wrote:
Are there other useful variants that are not currently supported (at least directly)?

I think the answer to this is “no.” My reasoning follows.

From the perspective of subclasses, superclass methods come in three sorts: overridable, augmentable, and final. The existing method declaration keywords support all possible transitions between these sorts, which suggests the current set is complete. However, my original email shows that compositions of these transitions can affect dispatch in different ways from the existing keywords, even if they result in methods belonging to the same sort.

This fundamentally hinges on the fact that overment has an unusually significant impact on dispatch behavior. overment switches from subclass-first dispatch to superclass-first dispatch, which means overment followed by augride effectively breaks a chain of Java-style methods into two sub-chains. In contrast, the other keywords provide no such utility:

  • override and augment on their own do not change dispatch characteristics at all from the perspective of other classes.

  • augride followed by overment creates a local chain of Java-style methods before switching back to Beta-style inheritance. Since this chain is wholly self-contained, it does not change the overall dispatch structure in any way, and it could be replaced by completely static dispatch with no loss of expressiveness.

The main interesting thing about overment followed by augride is that it allows something reminiscent of the CLOS :around method combination, since it allows a class to receive control on both the way down and the way up through method dispatch. Since override already provides the “on the way up” part, a hypothetical interface for this would make most sense as something that could be present on its own or in addition to an ordinary override declaration, like

    (define/override (m x)
      (super m x))
    (define/around (m x)
      (inner/around m x))

where inner/around is like inner, but it doesn’t take a default-expr, since it always has a Java-style method to dispatch to. I don’t know if these names are the best—around doesn’t really seem right to me—but I don’t know what else to call it.

Alexis

Reply all
Reply to author
Forward
0 new messages