A Taste of Nested Classes, part 2

(Continues from part 1)

Let’s now look at some message sends. Suppose we design the application so that the message respondToHit is sent to an asteroid when it’s hit by a missile. In Smalltalk, a typical response would look something like

respondToHit
    game
        replace: self with: self fragments;
        incrementScore.

game here is an instance variable holding a reference to the Game instance the asteroid belongs to, presumably initialized at the time the asteroid was created. How could we port this to Newspeak? Translating everything literally, we would define a slot named “game” in the Asteroid class and port its initialization logic. We would then port all the methods. The original respondToHit would remain unchanged, apart from the overall “packaging”:

respondToHit = (
    game
        replace: self with: self fragments;
        incrementScore.
)

But is it all the same as before? Not quite, because just like in Self, Newspeak slots are accessible only through messages. What looks like a good old game variable reference has now become a send of the message game to (an implicit) self.

Let’s hold this thought and backtrack to the end of the previous post. We said that an asteroid always knows the game instance it belongs to, it’s the enclosing object. We don’t need the game slot we ported from the Smalltalk original because it holds onto the same thing!

We can drop it and any of its initialization logic because the language now keeps track of the enclosing game instance for us. Instead suppose we define this method in Asteroid:

game = (
    "Get and return the enclosing object. To avoid getting ahead
    of the presentation, we don't show how it's done yet."
)

As for respondToHit and any other methods, they are unchanged—a perfect example of the benefits of representation independence. When game is just a message send, nobody cares how it gets its result.

Now, instead of revealing how the method game is implemented, we are going to do something even better. We are going to get rid of it altogether.

Time to talk about implicit receiver sends. I’ve hinted more than once that an implicit receiver is not always self. If it’s not self, what else can it be? Is there any other object that is somehow implicitly “present” at the location where the message is sent? Of course, it’s the enclosing object. (Or objects, if there is more than one layer of nested classes). Think of it this way: the respondToHit method is contained not only in the Asteroid class, but also indirectly in the Game class. So, the code in the method runs not only in the context of the current receiver, an instance of Asteroid, but also in the context of the enclosing instance of Game.

Without further ado I’m going to show respondToHit rewritten to take advantage of implicit receiver sends, and then describe their exact behavior. This will also explain under what specific conditions such a rewrite would be possible.

respondToHit = (
    replace: self with: fragments.
    incrementScore.
)

This reads almost like plain English. replace:with: and incrementScore are now sent to the enclosing game via the implicit receiver mechanism. Note that fragments is also changed to have an implicit receiver, though in this case we expect that the receiver is self. How does it happen that in both cases the actual receiver is what we want it to be?

Let’s recap how these potential receivers are related. We have essentially a queue of them: first self, then its enclosing object. (With more than two levels of nested classes, it would be followed by the enclosing object of the enclosing object and so on). If we represent the situation as a picture of the classes of the objects involved and their inheritance, we get this comb-like structure (to make the picture a little more interesting, Asteroid here is subclassed from a hypothetical ScreenObject, even though our original code didn’t say that).

Classes potentially involved in message processing

Because a method with a matching selector might be defined by any class in the comb, it may seem that the most intuitively reasonable lookup strategy is the one adopted by NewtonScript. The policy there was to search the entire comb starting with the receiver and its superclasses, then the receiver’s enclosing object (called parent in NewtonScript) with its superclasses, and so on. In the example we would look first in Asteroid, ScreenObject and Object with the intent of the Asteroid instance being the receiver, then Game and Object with the intent of the Game instance being the receiver. The send would fail if none of the classes in the comb implemented a method to handle the message.

I emphasized “seem” because the strategy in Newspeak is different, for the reason I am outlining in the end of the post. But first, here is how it really works:

  • If one of the classes on the “trunk” of the comb implements a method with a matching selector—in other words, if a matching method is lexically visible at the send site—the message is sent to the instance of that class.
  • Otherwise, the message is sent to self.

Even more informally, we could say that when by “looking around and up,” “around” meaning the same class definition and “up”—the definitions it’s nested in, we can see one with a matching method, the instance of that class is what receives the message. Otherwise, it is sent to self (and results in an MNU if self does not understand it).

So, the lookup in Newspeak differs from NewtonScript-like approach in two important ways. First, it doesn’t traverse the entire comb. The only potential handlers of the message are the methods defined in the trunk classes and those in the superclasses of the class of self. Second, lexical visibility takes priority over inheritance. If a method of Asteroid sends the message redraw to an implicit receiver and a method named redraw is defined in both Game and ScreenObject, the ScreenObject implementation wins in NewtonScript, while the one in Game wins in Newspeak.

This explains how the correct receiver is chosen in our implementation of respondToHit. As long as replace:with: and incrementScore are defined in Game, they are sent to the enclosing game instance as lexically visible. As for fragments, it’s sent to the asteroid no matter whether it’s defined in Asteroid or inherited. If Asteroid defines it, it’s lexically visible and the asteroid receives it according to the lexical visibility rule. If ScreenObject defines it (and assuming Game doesn’t), it’s lexically invisible and the asteroid receives it because the asteroid is self.

What if Game also had a method named fragments? In that case the definition in Game would win, so in order to get the fragments of the asteroid we would need to write the send explicitly as self fragments. What if replace:with: is inherited by Game from the superclass? In that case we also need to make the receiver explicit, because otherwise it would go to self:

game replace: self with: fragments.

This brings us back to the problem of writing a method named game returning the enclosing instance of Game. Now we know enough to come up with a very simple solution: we add the method to the class Game (not Asteroid), defined as

game = (
    ^self
)

So, now that we know how it all works—why does it work this way? Why bring lexical scoping of messages into the picture? Is it Rightâ„¢ to give priority to methods of another object over inherited methods of the receiver? Why not just follow the NewtonScript example—it appears simple and reasonable enough.

The thing is that compared to NewtonScript, class nesting in Newspeak is intended to solve a very different problem.

NewtonScript is prototype-based, and its object nesting is motivated by the need to represent the nesting of UI elements and support communication within that structure. The nesting and the associated message processing is essentially a built-in Chain of Responsibility pattern, very common in the UI field. (As an aside, Hopscotch implements NewtonScript-like mechanism for sending a message up the chain of nested UI elements. This is implemented as a simple library facility with no more language magic than doesNotUnderstand:. Implicit receivers further help make this very unobtrusive).

In Newspeak, even though the “free” back pointer from Asteroid to Game in our example was nice to have, having it was not the primary reason for nesting one class in the other. In fact, class nesting is a rather unwieldy mechanism for implementing arbitrary parent-child relationships—imagine having to define a group of classes for the scenario of “buttons and list boxes in a window,” and an entirely different one for “buttons and list boxes in a tab control in a window”!

Newspeak-like class nesting and method lookup are designed to handle relationships that have more to do with the modular structure of code, and with the organization of information sharing within a module. A is nested in B when B is a larger-scale component as far as the system architecture is concerned. Even though such nesting does effectively create a parent-child relationship between Bs and As, there are parent-child relationships that do not justify nesting.

In nested Newspeak classes, a child calls onto the functionality provided by the parent not as a fallback case in chain-of-responsibility processing, but as a way of interacting with its architectural context. The lookup policy supports that and at the same time allows to modularize the context. That is the motivation behind putting lexical visibility before inheritance.

If this sounds somewhat vague, I hope some of the more realistic examples in the next part will help clarify this point.

Continues in part 3.

9 Responses to “A Taste of Nested Classes, part 2”

  1. bob says:

    Thanks for the explanation, and, more importantly, discussing intended usage (and easy mis-use :-). It helps a lot.

    Would one rule be: if you want the contained instance to migrate to another container instance, don’t nest classes.

    I am somewhat concerned about one thing: it seems lexical visibility requires (a) container methods defined before contained methods, and (b) container methods to not be inherited from container super-classes. Are both of these true, and, if so, do they make some things more fragile?

  2. Gilad says:

    Bob,

    I’ll take the liberty of replying.
    (a) No.
    (b) If you make implicit receiver calls, yes. As the post explains – you don’t have to.

    As for fragility – (b) can cause fragility, but we trade off one kind of fragility for another. The comb rule (a la NewtonScript) means that adding methods to superclasses can silently “capture” lexically apparent names. In contrats, the Newspeak rules mean that only local changes (e.g., removing a lexically surrounding method) can have such effects. These are much easier to see and track.

  3. Danil says:

    Vassily, thank you for the article.

    Is it possible to apply class nesting for implementing of recursive (i.e. tree-like) structures of arbitrary depth?

  4. bob says:

    Thanks Gilad!

    Does that mean (a) you look-up container instances dynamically to find lexically visible methods, or (b) compiling a container class method somehow update contained class methods, or (c) something else? Can a contained instance migrate to another container instance?

  5. bob says:

    Ah, on a second read, it looks like: the look-up will (dynamically) chase the (static, lexically known) class chain of container classes, invoking the found method on the corresponding (dynamically found) container instance; otherwise does a self-send (really could be “super replace: self with: …”, since the immediate class was already checked).

    Hence migrating to a different container instance should not be allowed (unless with some type / class guarantees, which seems unlikely).

    Some kind of message-sequence diagram leading all the way to DNU would make this quite clear.

    Thanks

  6. steve says:

    I’m not clear what makes the class-containment chain any more “lexical” than the superclass-inheritance chain. Wouldn’t re-factoring a method to move it to a superclass cause behavior change with implicit receivers if that method was also defined in a containing class?

    Are there circumstances when you would recommend “always do an explicit self-send if …”?

  7. Vassili Bykov says:

    Steve:

    “Lexical” refers to relationships established by the structure of the source code. An inner class definition is contained in an outer class definition, so things in the outer class are called lexically visible from the inner class. In contrast, the superclass is only a reference in the source.

    Yes, moving an outer class method to a superclass may indeed cause a behavior change, so strictly speaking in Newspeak such a transformation is not a refactoring.

    The choice between implicit or explicit sends is essentially a question of API design. In a nutshell I’d say you design a module so that certain names are used within it to refer to certain things, and those are sent implicitly. I hope the next part will clarify this.

    Danil:

    No, that’s not something that class nesting is supposed to handle. Recursive structures can be created by simple object composition. Arbitrary depth, or varying composition patterns, or migration to different containers Bob suggested are all signs that the relationship between the classes is too “fluid” to consider one of them nested in another.

  8. steve says:

    That view of “lexical” seems suited to a primarily code-as-text world. But with IDEs that walk and present the underlying graph of objects in any number of suitable ways (hierarchy browser, traits browser) I am not sure how significant that is in terms of deciding tradeoffs. I do understand the API design angle, thanks.

  9. bob says:

    Steve: I think Newspeak superclasses can be determined dynamically (a “class-name” reference is in fact a method call), but class containment structure is statically fixed. Not sure if the superclass resolution is routinely re-evaluated as part of method lookup, or just resolved once when the class definition is evaluated and thereafter frozen. Makes my brain hurt a bit, in a mostly good way … so your point may be less valid in Newspeak than in some other languages.

Leave a Reply

For spam filtering purposes, please copy the number 7701 to the field below: