Search code examples
javascriptpythonoopinheritanceraku

What's the Raku equivalent of the super keyword as used in JavaScript and Python?


Whenever you extend a class in JavaScript or Python, the derived class must use the super keyword in order to set attributes and/or invoke methods and constructor in the base class. For example:

class Rectangle {
    constructor(length, width) {
        this.name = "Rectangle";
        this.length = length;
        this.width = width;
    }

    shoutArea() {
        console.log(
            `I AM A ${this.name.toUpperCase()} AND MY AREA IS ${this.length * this.width}`
        );
    }
    
    rectHello() {
        return "Rectanglish: hello";
    }
}

class Square extends Rectangle {
    constructor(length) {
        super(length, length);
        this.name = "Square"
    }
    
    squaHello() {
        const h = super.rectHello();
        return "Squarish:" + h.split(':')[1];
    }
}

const rect = new Rectangle(6, 4);
rect.shoutArea(); //=> I AM A RECTANGLE AND MY AREA IS 24

const squa = new Square(5);
squa.shoutArea(); //=> I AM A SQUARE AND MY AREA IS 25

console.log(squa.squaHello()); //=> Squarish: hello

Solution

  • What's the Raku equivalent of the super keyword as used in JavaScript and Python?

    One of Raku's re-dispatching functions

    Basics of redispatch

    First some code that does not include a redispatch function:

    class Rectangle {
      has ($.length, $.width)
    }
    
    Rectangle.new: length => 6, width => 4;
    

    The Rectangle declaration does not even include construction code, just a declaration of two attributes and that's it.

    So what is the Rectangle.new call doing? It's inheriting the default new method provided by Raku's Mu class which initializes any class attributes whose names match any named arguments.


    If you want a custom constructor that accepts positional arguments, then you typically write a new method which lists which arguments you want in its signature, and then have that method call suitably invoke the default new, which requires named arguments, by calling an appropriate redispatch function with the arguments converted to named arguments:

    class Rectangle {
      has ($.length, $.width);
      method new ($length, $width) { callwith length => $length, width => $width }
    }
    
    Rectangle.new: 6, 4;
    

    callwith is a redispatch function which does a:

    • call of the next matching candidate based on the original call.²

    • with a fresh set of arguments.

    In this simple case the original call was Rectangle.new: 6, 4, and the next candidate is the new method inherited from Mu.

    A Rectangle class based on yours

    Rather than mimic your code I'll write an idiomatic Raku translation of it and comment on it.

    class Rectangle {
      has ($!length, $!width) is required is built;
      method new ($length, $width) { callwith :$length, :$width }
      method shoutArea { put uc "I am a {self.^name} and my area is {$!length * $!width}" }
      method rectHello { 'Rectanglish: hello' }
    }
    
    constant rect = Rectangle.new: 6, 4;
    rect.shoutArea; #=> I AM A RECTANGLE AND MY AREA IS 24
    

    Commentary:

    • It's a good habit to default to writing code that limits problems that can arise as code evolves. For this reason I've used $!length for the length attribute rather than $.length

    • I've added an is required annotation to the attributes. This means a failure to initialize attributes by the end of an instance's construction will mean an exception gets thrown.

    • I've added an is built annotation to the attributes. This means that even an attribute without a public accessor -- as is the case for $!length and $!width due to my use of ! instead of . in the "twigil" -- can/will still be automatically initialized if there is a matching named argument in the construction call.

    • :$length is short for length => $length.

    • self.^name avoids unnecessary overhead. It's not important and quite possibly distracting to read about so feel free to ignore my footnote explaining it.⁴

    A Square class based on yours

    I'll make the new for Square redispatch:

    class Square is Rectangle {
      method new ($side-length) { callwith $side-length, $side-length }
      method squaHello { "Squarish: {self.rectHello.split(':')[1].trim}" }
    }
    
    constant squa = Square.new: 5;
    squa.shoutArea; #=> I AM A SQUARE AND MY AREA IS 25
    put squa.squaHello; #=> Squarish: hello
    

    Commentary:

    • I picked the name $side-length for the Square's .new parameter, but the name doesn't matter because it's a positional parameter/argument.

    • The redispatch is to the next candidate, just as it was before, abstractly speaking. Concretely speaking the next candidate this time is the method I had just defined in Rectangle (which in turn redispatches to the new of Mu).

    • self.rectHello suffices because the method being called has a different name than the originally called method (squaHello). If you renamed the two methods in Rectangle and Square to have the same name Hello then a redispatch would again be appropriate, though this time I'd have written just callsame rather than callwith ... because callsame just redispatches to the next candidate using the same arguments that were provided in the original call, which would save bothering to write out the arguments again.

    Footnotes

    ¹ Redispatching is a generalization of features like super. Redispatch functions are used for a range of purposes, including ones that have nothing to do with object orientation.

    ² In Raku a function or method call may result in the compiler generating a list of possibly matching candidates taking into account factors such as invocants for method calls, and multiple dispatch and function wrappers for both functions and methods. Having constructed a candidate list it then dispatches to the leading candidate (or the next one in the case of redispatch to the next candidate).

    ³ If you really want a getter/setter to be automatically generated for a given attribute, then declare it with a ., eg $.length instead of $!length, and Raku will generate both a $!length attribute and a .length getter. (And a setter too if you add an is rw to the $.length declaration.) I did this in the first code example to keep things a bit simpler.

    ⁴ The ^ in a method call like foo.^bar means a bar method call is redirected "upwards" (hence the ^) to the Higher Order Workings object that knows how a foo functions as a particular kind of type. In this case a Rectangle is a class and the HOW object is an instance of Perl6::Metamodel::ClassHOW, which knows how classes work, including that each class has a distinct name and has a .name method that retrieves that name. And the name of the Rectangle class is of course 'Rectangle', so self.^name saves having to create something else with the class's name.