1 of 24

Lecture 23

Subclassing, Overriding, Dynamic Dispatch

Programming Languages

UW CSE 341 - Spring 2021

2 of 24

Subclassing

  • Class definitions have a superclass, which affects the class:
    • Class inherits all methods from the superclass
    • But class can override methods as desired
    • (super-new …) ensures superclass initialization occurs
      • Creating the state the superclass needs
  • Unlike Java/C#/C++/etc.:
    • In Racket, subclassing has nothing to with a type system!
    • We don’t even have a type system in Racket ;)
    • This means we can (try to) send any message to any object

(define color-point%

(class point% …))

3 of 24

No accidental overrides

  • (define/public (foo …)) fails if foo is inherited
  • (define/override (foo …)) fails if foo is not inherited

This avoids accidental method-name collisions

But there is no perfect solution to all the “subclass can break if superclass changes” issues in OOP

4 of 24

super, final, pubment, augment, and more

Like in Java:

  • Overriding method can call method being overridden with super
  • Can prevent overriding with define/public-final

Unlike anything in Java:

  • Can have inner calls in superclass that call an augmentation in the subclass
    • Can allow multiple layers of subclass specialization while still ensuring superclass has control over what happens

See The Racket Guide and section materials

5 of 24

Simple Example: point% and color-point%

(define point%

(class object%

(super-new)

(init x y)

(define x-coord x)

(define y-coord y)

(define/public (get-x) x-coord)

(define/public (get-y) y-coord)

(define/public (dist-from-origin) …)

(define/public (dist-from-origin2) …)

(define/public (->string) …)))

6 of 24

Simple Example: point% and color-point%

(define point%

(class object%

(super-new)

(init x y)

(define x-coord x)

(define y-coord y)

(define/public (get-x) x-coord)

(define/public (get-y) y-coord)

(define/public (dist-from-origin) …)

(define/public (dist-from-origin2) …)

(define/public (->string) …)))

(define color-point%

(class point%

(super-new)

(init [color "black"])

(define current-color color)

(define/public (get-color) …)

(define/public (set-color! c) …)

(define/override (->string)

(string-append (get-color) ":"

(super ->string)))))

7 of 24

Every Object has a Class

  • Notice (new color-point% …) requires 3 init parameters
  • Note using is-a? is usually poor OOP style
    • Clients should just send messages to objects
    • Clients should not care how they define their behavior (abstraction)

(define p (new point% [x 1][y 1]))

(define cp (new color-point% [x 1][y 1][color "red"]))

(define y1 (is-a? p object%))

(define y2 (is-a? p point%))

(define n1 (is-a? p color-point%))

(define y3 (is-a? cp object%))

(define y4 (is-a? cp point%))

(define y5 (is-a? cp color-point%))

8 of 24

About that color-point% subclass

In our example, we assume:

  • point% was already defined
  • We wanted a class with the functionality of color-point%

Under these assumptions, in this case, subclassing was a good choice, but there are alternatives we should consider

  • Especially since programmers often overuse subclassing

9 of 24

Alternative #1: Edit the superclass

Add to point%:

With default value for init parameter color, we can “almost get away with this”

Problems:

  • Breaks existing subclasses that define/public get-color or set-color!
  • Need ugly hacks to preserve ->string behavior for uncolored points
  • Just “poor style” to have color state for uncolored points

(init [color "black"])

(define current-color color)

(define/public (get-color) …)

(define/public (set-color! c) …)

10 of 24

Alternative #2: Copy/paste

Make color-point% a subclass of object% and just copy/paste most of the definition of point%

Note this does basically work:

  • No type system in the way (any message to any object)
  • Only difference is getting #f for (is-a? (new color-point% …)

point%)

But:

  • Code reuse is nice for the usual reasons

(define color-point%

(class object%

(super-new)

(init x y)

(define x-coord x)

(define y-coord y)

(define/public (get-x) x-coord)

(define/public (get-y) y-coord)

(define/public (dist-from-origin) …)

(define/public (dist-from-origin2) …)

(init [color "black"])

(define current-color color)

(define/public (get-color) …)

(define/public (set-color! c) …)

(define/public (->string) …)))

11 of 24

Alternative #3: Use a point% object in private state

Instead of subclassing, could just have a point% “private field” instead!

Implementation has to “forward” method calls to underlying point, i.e., no syntactic convenience of inheritance, but this approach also has flexibility (not needed here)

(define color-point%

(class object%

(super-new)

(init x y)

(define point (new point% [x x][y y]))

(define/public (get-x) (send point get-x)

(define/public (get-y) (send point get-y)

... ; more “forwarding messages”

(init [color "black"])

(define current-color color)

(define/public (get-color) …)

(define/public (set-color! c) …)

(define/public (->string) …)))

12 of 24

Subtleties of Object Initialization

See code for a variant of point% that does (send this ->string) during object initialization

If not careful, a color-point% subclass is broken because:

  • It overrides ->string
  • The overriding method accesses private state that needs to be initialized

This is why Racket lets (super-new) appear anywhere in the initialization order

  • Sometimes needs to not be first (!) so needed state exists “in time”
  • Other languages (e.g., Java) less flexible

Overall, self calls during object initialization particularly tricky in OOP

  • Avoid them if you can (?)

13 of 24

What about 3D-Points? (See code)

Having point-3d% subclass point% is highly questionable style

  • Does get some code reuse, so “tempting”
    • “works” with enough overriding and use of super
  • From an “is-a?” perspective, does it really “behave like a 2D point”?
    • No, breaks invariants like “if x and y coordinates are 0, then distance to origin is 0”
    • Colored points didn’t change behavior like this

14 of 24

Now Polar-Points

Alternate representation/implementation of the same abstraction

    • Private state of “radius and angle” instead of “x and y”

Do not have to subclass, but by doing so a couple fascinating things:

  • Superclass private state is never used
    • But Racket still requires (super-new)
  • We don’t have to override dist-from-origin2 (!!!)
    • Overriding is much more powerful and semantically interesting when superclass methods have self calls to methods we override

15 of 24

The Key Point re: Overriding

In many cases, objects are not so different from closures:

  • Have multiple methods rather than one “call me”
  • Inheritance avoids code copying
  • But really a lot like a “record of functions that share an environment”

But overriding methods called by other superclass methods is fundamentally different

  • The essential difference of OOP
    • The semantics needs careful study
    • Like many language features, both powerful and error-prone

16 of 24

Dynamic Dispatch

  • Many synonymous names: “late binding”, “virtual methods”, etc.
  • In a class c%, calling method m can resolve to a version of m defined in a subclass of c%
  • Perhaps the most unique characteristic of Object-Oriented Programming

Now: define the semantics of method resolution precisely

17 of 24

Review: Variable Lookup

To evaluate a variable expression foo:

  • Look up foo in the appropriate (dynamic) environment
  • Use lexical scope for closures!

18 of 24

this

  • this refers to the “current object”
    • So “current object” needs to be part of the environment (when evaluating a method call body)
  • Use the “current object” to get the “right” private state
  • And use the “current object’s” class to determine “what method to call” for a message send

19 of 24

Method Lookup

(send e0 m e1 … eN)

  • Evaluate e0, e1, …, eN to values v0, v1, …, vN
    • If v0 is not an object, error
  • Let c be the class of v0
  • If c defines m, chose that definition
    • Else, recursively search superclass of c
    • If search reaches object% without finding m, error
  • Call chosen definition of m with v1, …, vN bound as arguments
    • And with this bound to v0

20 of 24

Method Lookup

(send e0 m e1 … eN)

  • Evaluate e0, e1, …, eN to values v0, v1, …, vN
    • If v0 is not an object, error
  • Let c be the class of v0
  • If c defines m, chose that definition
    • Else, recursively search superclass of c
    • If search reaches object% without finding m, error
  • Call chosen definition of m with v1, …, vN bound as arguments
    • And with this bound to v0

Implements dynamic dispatch!

21 of 24

Punchline

This is why dist-from-origin2 “just worked”

  • this definition gets bound to receiver, thus calls get-x and get-y defined in subclass

Notes:

  • Roughly as complex as lexical scope even if you learned it earlier
  • Like lexical scope, must understand precisely!
  • Not lexical scope: this is treated specially in the semantics

22 of 24

“Static Overloading”

In Java, method lookup is slightly more complex:

  • More than 1 method in a class can have the same name!

Java solution:

  • Treat argument types as part of a method’s name
  • Only override when exact same number and types of arguments
  • Overloading convenient, but fundamentally relies on static types
    • Makes no sense in Racket!
    • Also subtle rules for when multiple methods might “match” in order to pick the “best” one
      • Or give a type error when there is no unique best

23 of 24

The OOP Tradeoff

A method m that calls other overridable methods can have its behavior changed in any subclass!

  • Even if m itself is not overridden, so subclasses may or may not intend to change its behavior

This makes it harder to reason about the code you’re looking at 😬

  • Can be mitigated with “final” methods (see Racket/Java docs)

OTOH, easier for subclasses to customize behavior without copy/pasting code! 😃

  • In other words, this behavior is often “used on purpose”

24 of 24

Lexical scope does not do that

In functional programming, if an immutable variable is bound to a function in an environment, it is always bound to that function

  • Easier to reason about code locally
  • Harder to change behavior of code non-locally