Raku by Example: Classes and Objects

Class definition and attributes

Classes are defined with the class keyword. By default, all attributes are private attributes (meaning they're only accessible within the class they're defined) and are declared with the has keyword and the ! twigil. Declaring an attribute with the . twigil declares a public attribute (also known as a private attribute with a read-only accessor method named after the attribute).

With each new class, Raku automatically provides a new constructor which takes named arguments and uses them to initialize the public attributes.

class Point2D {
    has Str $!id = 'P2'; # private attr. with default value
    has Numeric $.x;     # attr. with read-only accesor
    has Numeric $.y;     # same as above
}

# Initializing an object instance. Private attributes
# cannot be initialized during object construction.
my $p1 = Point2D.new(x => 3, y => 4);

say "x: ", $p1.x; # invoking the read-only accessor
say "y: ", $p1.y; # same as above

# An attribute with a read-only accesor cannot be modified
# from outside the class. To do so, add the `is rw` trait 
# to make it read-write. For example: `has Numeric $.x is rw;`
$ perl6 class.p6
x: 3
y: 4

Methods

Methods are declared with the method keyword. By default, all methods are public. To make a method private, prepend a ! to the method's name. Similar to subroutines, methods can have typed parameters, typed returns, be multi, etc.

class Point2D {
    has Str $!id;
    has Numeric $.x;
    has Numeric $.y;
    
    method rect-coordinates {
        return (self.x, self.y); # Or: ($.x, $.y)
    }
    
    # notice the ! before the method's name
    method !distance-to-center {
        # public attr. can be accessed with the ! twigil
        return sqrt($!x ** 2 + $!y ** 2);
    }
    
    method polar-coordinates {
        # calling a method is done on `self` or an explicit invocant.
        # use `!` for a private attr. and a `.` for a public attr.
        
        my $radius = self!distance-to-center; # calling a private method
        my $theta = atan2($!y, $!x);
        return $radius, $theta; 
    }
}

my $p2 = Point2D.new(:x(3), :y(4)); # using the colon-pair syntax

say "Cartesian coordinates: ", $p2.rect-coordinates;
say "Polar coordinates: ",     $p2.polar-coordinates;
$ perl6 class.p6
Cartesian coordinates: (3 4)
Polar coordinates: (5 0.9272952180016122)

Class and instance methods

A method's signature can have an explicit invocant as its first parameter which is created by placing a colon after it. This allows for the method to refer to the object it was called on explicitly. Furthermore, the ::?CLASS variable can be used, combined with either :U (for class methods) and :D (for instance methods).

class Point2D {
    has Str $!id;
    has Numeric $.x;
    has Numeric $.y;
   
    # this is a class method. it's an alternate
    # constructor method, only callable on the class.
    method create-point(::?CLASS:U $POINT: $abs, $ord) {
                                   #^^^^^ Explicit invocant
        $POINT.new(x => $abs, y => $ord);
    }
   
    # Another construct but instead of using `new` as intermediary,
    # we're calling the `bless` method which `new` calls anyway.
    method point(::?CLASS:U: $abs, $ord ) {
       # `bless` must always receive named arguments.
       self.bless( x => $abs, y => $ord);  
    }
    
    method rect-coordinates {
        return (self.x, self.y);
    }
    
    method !distance-to-center {
        return sqrt($!x ** 2 + $!y ** 2);
    }
    
    method polar-coordinates {
        my $radius = self!distance-to-center;
        my $theta = atan2 $!y, $!x;
        return $radius, $theta; 
    }
}

my $p3   = Point2D.create-point(8, 12);
my $p3_a = Point2D.point(-2, 3);

say "Cartesian coordinates: ", $p3.coordinates;
say "Cartesian coordinates: ", $p3_a.coordinates;
$ perl6 class.p6
Cartesian coordinates: (8 12)
Cartesian coordinates: (-2 3)

Submethods

Submethods are public methods that are not inherited by subclasses. Two notable examples of submethods are

  • BUILD - useful for object construction and destruction tasks.
  • TWEAK - ideal for checking things or modify attributes after object construction.
class Point2D {
    has Str $!id;
    has Numeric $.x;
    has Numeric $.y;
    
    submethod TWEAK {
        # assign an id after object construction
        $!id = ('A'..'Z', 0..9).flat.roll(6).join ~ 'P2';
    }

    method get-id {
        return $!id;
    }

    method create-point(::?CLASS:U $POINT: $abs, $ord) {
        $POINT.new(x => $abs, y => $ord);
    }
   
    method point(::?CLASS:U: $abs, $ord ) {
       self.bless( x => $abs, y => $ord);  
    }
    
    method rect-coordinates {
        return (self.x, self.y);
    }
    
    method !distance-to-center {
        return sqrt($!x ** 2 + $!y ** 2);
    }
    
    method polar-coordinates {
        my $radius = self!distance-to-center;
        my $theta = atan2 $!y, $!x;
        return $radius, $theta; 
    }
}

my $p3_c = Point2D.create-point(-5, 2);
say "Point at {$p3_c.rect-coordinates} has ID: {$p3_c.get-id}";
$ perl6 class.p6
Point at -5 2 has ID: O76M9CP2

Inheritance

Inheritance is achieved in Raku by using the is keyword.

sub MovablePoint is Point2D {
    has Numeric $.x is rw; # child's attributes are
    has Numeric $.y is rw; # now read-write
    
    # translate both vertically and horizontally
    multi method translate( Numeric $abs, Numeric $ord ) {
        $!x += $abs;
        $!y += $ord;
        Nil;
    }
   
    # translate horizontally
    multi method translate( Numeric $abs ) {
        $!x += $abs;
        Nil;
    }
}

my $p4 = MovablePoint.new(:x(12), :y(-5));

say "Coordinates: ", $p4.rect-coordinates;
say "Polar coordinates: ", $p4.polar-coordinates;

say "---> Translating point...";
$p4.translate(5, 6);
# or: `$p4.x = 5; $p4.y = 6;` since
# accessors are now read-write

say "New coordinates: ", $p4.rect-coordinates;
say "New polar coordinates: ", $p4.polar-coordinates;
$ perl6 class.p6

Coordinates: (12 -5)
Polar coordinates: (13 -0.3947911196997615)
---> Translating point...
New coordinates: (17 1)
New polar coordinates: (13 -0.3947911196997615)

While the rectangular coordinates changed when the point moved, the polar coordinates didn't change due to the way the values of x and y are accessed in the parent class Point2D from which MovablePoint inherits. In the rect-coordinates method, x and y are accessed through their accessor methods (self.x and self.y) while in the polar-coordinates they're accessed directly ($!x and $!y). Keep this in mind if you don't plan on overriding the inherited methods in the child class.

Roles

A role is defined with the role keyword and applied with does.

role Colored {
    has %.color;

    multi method change-color( Int $red, Int $green, Int $blue ) {
        given self {
            .color<red>   = $red;
            .color<green> = $green;
            .color<blue>  = $blue;
        }
        Nil;
    }

    multi method change-color( $point: %hue ) {
        given $point {
            .color<red>   = %hue<red>   // 0;
            .color<green> = %hue<green> // 0;
            .color<blue>  = %hue<blue>  // 0;
        }
        Nil;
    }
}

# Application of a role during object initialization:
my $p5 = Point2D.new(:x(9), :y(18)) does Colored;

say "Color: ", $p5.color ?? $p5.color !! "Not Yet";
$p5.change-color(220, 25, 78);
say "New Color: ", $p5.color;
$p5.change-color({red => 10, green => 50, blue => 120});
say "Another color: ", $p5.color;

# Application of a role to a class:
class ColoredPoint is Point2D does Colored {

}

my $p6 = ColoredPoint.new(
    :x(9),
    :y(18),
    color => { :red(45), :green(56), :blue(200) }
);

say "Pt. 6's color: ", $p6.color;
say $p6;
$ perl6 class.p6

Color: Not Yet
New Color: {blue => 78, green => 25, red => 220}
Another color: {blue => 120, green => 50, red => 10}
Pt. 6's color: {blue => 200, green => 56, red => 45}
ColoredPoint.new(color => {:blue(200), :green(56), :red(45)}, x => 9, y => 18)

Changing an object's default representation

Each user-defined class has a gist method that provides a default representation of the class. If a different representation is needed, this method can be overriden to prepare the output as desired.

# Let's use the `ColoredPoint` class. Both `Point2D` and `Colored` are
# implied to be present here.

class ColoredPoint is Point2D does Colored {
    method gist {
         qq:to/EOF/;
         πŸ—ΊοΈ  ($.x, $.y)
         πŸ”΄  %!color<red>
         🍏  %!color<green>
         πŸ”΅  %!color<blue>
         EOF
    }
}

my $colored_point = ColoredPoint.new(
    :x(9),
    :y(18),
    color => { :red(45), :green(56), :blue(200) }
);

say $colored_point;  # printing out the object!
$ perl6 class.p6

πŸ—ΊοΈ  (9, 18)
πŸ”΄  45
🍏  56
πŸ”΅  200

Introspection

Introspection is the process of gathering information about some objects in a program.

Given an object, some questions can be asked. Few of them are:

  • ~~ - smartmatch the object against a class. If the object is of that class, or of an inheriting class, it returns True.

  • .gist - returns a default and terse representation of an object, though it can be overriden to provide the desired representation. The sub (or method) say calls this method by default.

  • .perl - returns a string that can be executed as Perl code, and reproduces the original object.

  • .^name - calls the name method on the meta object and returns the class name.

  • .attributes - calls the attributes method on the meta object and returns all attributes of the object, although providing the :local named argument limits the returned attributes to those defined in the class.

  • .^methods - calls the methods method on the meta object and returns a list of the methods callable on the object. Using the :local named argument limits the returned methods to those defined in the class.

  • .^parents - calls the parents method on the meta object and returns the parent classes of the object's class.

The syntax of calling a method with .^ instead of just . means that it is actually a method call on its meta class, which is a class managing the properties any given class. To learn more about, head over to https://docs.perl6.org/language/mop.

my $obj = ColoredPoint.new(
    :x(9),
    :y(18),
    color => { :red(45), :green(56), :blue(200) }
);


if $obj ~~ ColoredPoint {
    say "It's a colored point."
};

if $obj ~~ Point2D {
    say "It's also a point since it inherits from Point2D."
};

say $obj.gist;
say $obj.perl;

say $obj.^name;               # name of the object's class

say $obj.^attributes;         # all attributes including parent class'
say $obj.^attributes(:local); # only in-class defined attributes

say $obj.^methods;            # all methods including parent class'
say $obj.^methods(:local);    # only methods defined in the class

say $obj.^parents;            # parent class(es)
$ perl6 introspect.p6
It's a colored point.
It's also a point since it inherits from Point2D.
πŸ—ΊοΈ  (9, 18)
πŸ”΄  45
🍏  56
πŸ”΅  200

ColoredPoint.new(color => {:blue(200), :green(56), :red(45)}, x => 9, y => 18)
ColoredPoint
(Associative %!color Str $!id Numeric $!x Numeric $!y)
(Associative %!color)
(gist change-color color BUILDALL x rect-coordinates get-id y polar-coordinates point create-point BUILDALL TWEAK)
(gist change-color color BUILDALL)
((Point2D))

For more info, go to

Back to main