Skip to main content
Engineering LibreTexts

5.6: Method Lookup Follows the Inheritance Chain

  • Page ID
    36359
  • \( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \) \( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)\(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\) \(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\)\(\newcommand{\AA}{\unicode[.8,0]{x212B}}\)

    What exactly happens when an object receives a message?

    The process is quite simple: the class of the receiver looks up the method to use to handle the message. If this class does not have a method, it asks its superclass, and so on, up the inheritance chain. When the method is found, the arguments are bound to the parameters of the method, and the virtual machine executes it.

    It is essentially as simple as this. Nevertheless there are a few questions that need some care to answer:

    • What happens when a method does not explicitly return a value?
    • What happens when a class reimplements a superclass method?
    • What is the difference between self and super sends?
    • What happens when no method is found?

    The rules for method lookup that we present here are conceptual: virtual machine implementors use all kinds of tricks and optimizations to speed-up method lookup. That’s their job, but you should never be able to detect that they are doing something different from our rules.

    First let us look at the basic lookup strategy, and then consider these further questions.

    Method lookup

    Suppose we create an instance of EllipseMorph.

    anEllipse := EllipseMorph new.
    

    If we now send this object the message defaultColor, we get the result Color yellow.

    anEllipse defaultColor    → Color yellow 
    

    The class EllipseMorph implements defaultColor, so the appropriate method is found immediately.

    Code \(\PageIndex{1}\) (Squeak): A Locally Implemented Method

    EllipseMorph»defaultColor
        "answer the default color/fill style for the receiver"
        ↑ Color yellow
    

    In contrast, if we send the message openInWorld to anEllipse, the method is not immediately found, since the class EllipseMorph does not implement openInWorld. The search therefore continues in the superclass, BorderedMorph, and so on, until an openInWorld method is found in the class Morph (see Figure \(\PageIndex{1}\)).

    Code \(\PageIndex{2}\) (Squeak): An Inherited Method

    Morph»openInWorld
        "Add this morph to the world. If in MVC, then provide a Morphic window for it."
        self couldOpenInMorphic
            ifTrue: [self openInWorld: self currentWorld]
            ifFalse: [self openInMVC]
    
    Method lookup follows the inheritance hierarchy.
    Figure \(\PageIndex{1}\): Method lookup follows the inheritance hierarchy.

    Returning self

    Notice that EllipseMorph»defaultColor (Code \(\PageIndex{1}\)) explicitly returns Color yellow whereas Morph»openInWorld (Code \(\PageIndex{2}\)) does not appear to return anything.

    Actually a method always answers a message with a value — which is, of course, an object. The answer may be defined by the \(\uparrow\) construct in the method, but if execution reaches the end of the method without executing a \(\uparrow\), the method still answers a value: it answers the object that received the message. We usually say that the method “answers self”, because in Smalltalk the pseudo-variable self represents the receiver of the message, rather like this in Java.

    This suggests that Code \(\PageIndex{2}\) is equivalent to Code \(\PageIndex{3}\):

    Code \(\PageIndex{3}\) (Squeak): Explicitly Returning self

    Morph»openInWorld
        "Add this morph to the world. If in MVC,
        then provide a Morphic window for it."
        self couldOpenInMorphic
            ifTrue: [self openInWorld: self currentWorld]
            ifFalse: [self openInMVC].
        ↑ self "Don't do this unless you mean it"
    

    Why is writing self explicitly not a good thing to do? Well, when you return something explicitly, you are communicating that you are returning something of interest to the sender. When you explicitly return self, you are saying that you expect the sender to use the returned value. This is not the case here, so it is best not to explicitly return self.

    This is a common idiom in Smalltalk, which Kent Beck refers to as “Interesting return value”1:

    Return a value only when you intend for the sender to use the value.

    Overriding and extension

    If we look again at the EllipseMorph class hierarchy in Figure \(\PageIndex{1}\), we see that the classes Morph and EllipseMorph both implement defaultColor. In fact, if we open a new morph (Morph new openInWorld) we see that we get a blue morph, whereas an ellipse will be yellow by default.

    We say that EllipseMorph overrides the defaultColor method that it inherits from Morph. The inherited method no longer exists from the point of view of anEllipse.

    Sometimes we do not want to override inherited methods, but rather extend them with some new functionality, that is, we would like to be able to invoke the overridden method in addition to the new functionality we are defining in the subclass. In Smalltalk, as in many object-oriented languages that support single inheritance, this can be done with the help of super sends.

    The most important application of this mechanism is in the initialize method. Whenever a new instance of a class is initialized, it is critical to also initialize any inherited instance variables. However, the knowledge of how to do this is already captured in the initialize methods of each of the superclass in the inheritance chain. The subclass has no business even trying to initialize inherited instance variables!

    It is therefore good practice whenever implementing an initialize method to send super initialize before performing any further initialization:

    Code \(\PageIndex{4}\) (Squeak): Super Initialize

    BorderedMorph»initialize
        "initialize the state of the receiver"
        super initialize.
        self borderInitialize
    

    An initialize method should always start by sending super initialize.

    Self sends and super sends

    We need super sends to compose inherited behaviour that would otherwise be overridden. The usual way to compose methods, whether inherited or not, however, is by means of self sends.

    How do self sends differ from super sends? Like self, super represents the receiver of the message. The only thing that changes is the method lookup. Instead of lookup starting in the class of the receiver, it starts in the superclass of the class of the method where the super send occurs.

    Note that super is not the superclass! It is a common and natural mistake to think this. It is also a mistake to think that lookup starts in the superclass of the receiver. We shall see with the following example precisely how this works.

    Consider the message initString, which we can send to any morph:

    anEllipse initString → '(EllipseMorph newBounds: (0@0 corner: 50@40) color:
        Color yellow) setBorderWidth: 1 borderColor: Color black'
    

    The return value is a string that can be evaluated to recreate the morph.

    How exactly is this result obtained through a combination of self and super sends? First, anEllipse initString will cause the method initString to be found in the class Morph, as shown in Figure \(\PageIndex{2}\).

    self and super sends.
    Figure \(\PageIndex{2}\): self and super sends.

    Code \(\PageIndex{5}\) (Squeak): A self Send

    Morph»initString
        ↑ String streamContents: [:s | self fullPrintOn: s]
    

    The method Morph»initString performs a self send of fullPrintOn:. This causes a second lookup to take place, starting in the class EllipseMorph, and finding fullPrintOn: in BorderedMorph (see Figure \(\PageIndex{2}\) once again). What is critical to notice is that the self send causes the method lookup to start again in the class of the receiver, namely the class of anEllipse.

    A self send triggers a dynamic method lookup starting in the class of the receiver.

    Code \(\PageIndex{6}\) (Python): Combining super and self Sends

    BorderedMorph»fullPrintOn: aStream
        aStream nextPutAll: '('.
        super fullPrintOn: aStream.
        aStream nextPutAll: ') setBorderWidth: '; print: borderWidth;
            nextPutAll: ' borderColor: ' , (self colorString: borderColor)
    

    At this point, BorderedMorph»fullPrintOn: does a super send to extend the fullPrintOn: behaviour it inherits from its superclass. Because this is a super send, the lookup now starts in the superclass of the class where the super send occurs, namely in Morph. We then immediately find and evaluate Morph»fullPrintOn:.

    Note that the super lookup did not start in the superclass of the receiver. This would have caused lookup to start from BorderedMorph, resulting in an infinite loop!

    A super send triggers a static method lookup starting in the superclass of the class of the method performing the super send.

    If you think carefully about super send and Figure \(\PageIndex{2}\), you will realize that super bindings are static: all that matters is the class in which the text of the super send is found. By contrast, the meaning of self is dynamic: it always represents the receiver of the currently executing message. This means that all messages sent to self are looked-up by starting in the receiver’s class.

    Message not understood

    What happens if the method we are looking for is not found?

    Suppose we send the message foo to our ellipse. First the normal method lookup would go through the inheritance chain all the way up to Object (or rather ProtoObject) looking for this method. When this method is not found, the virtual machine will cause the object to send self doesNotUnderstand: #foo. (See Figure \(\PageIndex{3}\))

    Message foo is not understood.
    Figure \(\PageIndex{3}\): Message foo is not understood.

    Now, this is a perfectly ordinary, dynamic message send, so the lookup starts again from the class EllipseMorph, but this time searching for the method doesNotUnderstand:. As it turns out, Object implements doesNotUnderstand:. This method will create a new MessageNotUnderstood object which is capable of starting a Debugger in the current execution context.

    Why do we take this convoluted path to handle such an obvious error? Well, this offers developers an easy way to intercept such errors and take alternative action. One could easily override the method doesNotUnderstand: in any subclass of Object and provide a different way of handling the error.

    In fact, this can be an easy way to implement automatic delegation of messages from one object to another. A Delegator object could simply delegate all messages it does not understand to another object whose responsibility it is to handle them, or raise an error itself!


    1. Kent Beck, Smalltalk Best Practice Patterns. Prentice-Hall, 1997.


    This page titled 5.6: Method Lookup Follows the Inheritance Chain is shared under a CC BY-SA 3.0 license and was authored, remixed, and/or curated by Andrew P. Black, Stéphane Ducasse, Oscar Nierstrasz, Damien Pollet via source content that was edited to the style and standards of the LibreTexts platform; a detailed edit history is available upon request.