declarator role

Documentation for declarator role, assembled from the following types:

language documentation Object Orientation

From Object Orientation

(Object Orientation) declarator role

Roles are in some ways similar to classes, in that they are a collection of attributes and methods. They differ in that roles are also meant for describing only parts of an object's behavior and in how roles are applied to classes. Or to phrase it differently, classes are meant for managing objects and roles are meant for managing behavior and code reuse.

use MONKEY-SEE-NO-EVAL;
role Serializable {
    method serialize() {
        self.perl# very primitive serialization 
    }
    method deserialize($buf{
        EVAL $buf# reverse operation to .perl 
    }
}
 
class Point does Serializable {
    has $.x;
    has $.y;
}
my $p = Point.new(:x(1), :y(2));
my $serialized = $p.serialize;
my $clone-of-p = Point.deserialize($serialized);
say $clone-of-p.x# OUTPUT: «1␤» 

Roles are immutable as soon as the compiler parses the closing curly brace of the role declaration.

Application of Role

Role application differs significantly from class inheritance. When a role is applied to a class, the methods of that role are copied into the class. If multiple roles are applied to the same class, conflicts (e.g. attributes or non-multi methods of the same name) cause a compile-time error, which can be solved by providing a method of the same name in the class.

This is much safer than multiple inheritance, where conflicts are never detected by the compiler, but are instead resolved to the superclass that appears earlier in the method resolution order, which might not be what the programmer wanted.

For example, if you've discovered an efficient method to ride cows, and are trying to market it as a new form of popular transportation, you might have a class Bull, for all the bulls you keep around the house, and a class Automobile, for things that you can drive.

class Bull {
    has Bool $.castrated = False;
    method steer {
        # Turn your bull into a steer 
        $!castrated = True;
        return self;
    }
}
class Automobile {
    has $.direction;
    method steer($!direction{ }
}
class Taurus is Bull is Automobile { }
 
my $t = Taurus.new;
say $t.steer;
# OUTPUT: «Taurus.new(castrated => Bool::True, direction => Any)␤» 

With this setup, your poor customers will find themselves unable to turn their Taurus and you won't be able to make more of your product! In this case, it may have been better to use roles:

role Bull-Like {
    has Bool $.castrated = False;
    method steer {
        # Turn your bull into a steer 
        $!castrated = True;
        return self;
    }
}
role Steerable {
    has Real $.direction;
    method steer(Real $d = 0{
        $!direction += $d;
    }
}
class Taurus does Bull-Like does Steerable { }

This code will die with something like:

===SORRY!===
Method 'steer' must be resolved by class Taurus because it exists in
multiple roles (SteerableBull-Like)

This check will save you a lot of headaches:

class Taurus does Bull-Like does Steerable {
    method steer($direction?{
        self.Steerable::steer($direction)
    }
}

When a role is applied to a second role, the actual application is delayed until the second role is applied to a class, at which point both roles are applied to the class. Thus

role R1 {
    # methods here 
}
role R2 does R1 {
    # methods here 
}
class C does R2 { }

produces the same class C as

role R1 {
    # methods here 
}
role R2 {
    # methods here 
}
class C does R1 does R2 { }

Stubs

When a role contains a stubbed method, a non-stubbed version of a method of the same name must be supplied at the time the role is applied to a class. This allows you to create roles that act as abstract interfaces.

role AbstractSerializable {
    method serialize() { ... }        # literal ... here marks the 
                                      # method as a stub 
}
 
# the following is a compile time error, for example 
#        Method 'serialize' must be implemented by Point because 
#        it's required by a role 
 
class APoint does AbstractSerializable {
    has $.x;
    has $.y;
}
 
# this works: 
class SPoint does AbstractSerializable {
    has $.x;
    has $.y;
    method serialize() { "p($.x$.y)" }
}

The implementation of the stubbed method may also be provided by another role.

Inheritance

Roles cannot inherit from classes, but they may cause any class which does that role to inherit from another class. So if you write:

role A is Exception { }
class X::Ouch does A { }
X::Ouch.^parents.say # OUTPUT: «((Exception))␤» 

then X::Ouch will inherit directly from Exception, as we can see above by listing its parents.

Pecking order

A method defined directly in a class will always override definitions from applied roles or from inherited classes. If no such definition exists, methods from roles override methods inherited from classes. This happens both when said class was brought in by a role, and also when said class was inherited directly.

role M {
  method f { say "I am in role M" }
}
 
class A {
  method f { say "I am in class A" }
}
 
class B is A does M {
  method f { say "I am in class B" }
}
 
class C is A does M { }
 
B.new.f# OUTPUT «I am in class B␤» 
C.new.f# OUTPUT «I am in role M␤» 

Note that each candidate for a multi-method is its own method. In this case, the above only applies if two such candidates have the same signature. Otherwise, there is no conflict, and the candidate is just added to the multi-method.

Automatic Role Punning

Any attempt to directly instantiate a role, as well as many other operations on it, will automatically create an instance of a class with the same name as the role, making it possible to transparently use a role as if it were a class.

role Point {
    has $.x;
    has $.y;
    method abs { sqrt($.x * $.x + $.y * $.y}
}
say Point.new(x => 6=> 8).abs# OUTPUT «10␤» 

We call this automatic creation of classes punning, and the generated class a pun.

Parameterized Roles

Roles can be parameterized, by giving them a signature in square brackets:

role BinaryTree[::Type{
    has BinaryTree[Type$.left;
    has BinaryTree[Type$.right;
    has Type $.node;
 
    method visit-preorder(&cb{
        cb $.node;
        for $.left$.right -> $branch {
            $branch.visit-preorder(&cbif defined $branch;
        }
    }
    method visit-postorder(&cb{
        for $.left$.right -> $branch {
            $branch.visit-postorder(&cbif defined $branch;
        }
        cb $.node;
    }
    method new-from-list(::?CLASS:U: *@el{
        my $middle-index = @el.elems div 2;
        my @left         = @el[0 .. $middle-index - 1];
        my $middle       = @el[$middle-index];
        my @right        = @el[$middle-index + 1 .. *];
        self.new(
            node    => $middle,
            left    => @left  ?? self.new-from-list(@left)  !! self,
            right   => @right ?? self.new-from-list(@right!! self,
        );
    }
}
 
my $t = BinaryTree[Int].new-from-list(456);
$t.visit-preorder(&say);    # OUTPUT: «5␤4␤6␤» 
$t.visit-postorder(&say);   # OUTPUT: «4␤6␤5␤» 

Here the signature consists only of a type capture, but any signature will do:

enum Severity <debug info warn error critical>;
 
role Logging[$filehandle = $*ERR{
    method log(Severity $sev$message{
        $filehandle.print("[{uc $sev}$message\n");
    }
}
 
Logging[$*OUT].log(debug'here we go'); # OUTPUT: «[DEBUG] here we go␤» 

You can have multiple roles of the same name, but with different signatures; the normal rules of multi dispatch apply for choosing multi candidates.

Mixins of Roles

Roles can be mixed into objects. A role's given attributes and methods will be added to the methods and attributes the object already has. Multiple mixins and anonymous roles are supported.

role R { method Str() {'hidden!'} };
my $i = 2 but R;
sub f(\bound){ put bound };
f($i); # OUTPUT: «hidden!␤» 

Note that the object got the role mixed in, not the object's class or the container. Thus, @-sigiled containers will require binding to make the role stick. Some operators will return a new value, which effectively strips the mixin from the result.

Mixins can be used at any point in your object's life.

# A counter for Table of Contents 
role TOC-Counter {
    has Int @!counters is default(0);
    method Str() { @!counters.join: '.' }
    method inc($level{
        @!counters[$level - 1]++;
        @!counters.splice($level);
        self
    }
}
 
my Num $toc-counter = NaN;     # don't do math with Not A Number 
say $toc-counter;              # OUTPUT: «NaN␤» 
$toc-counter does TOC-Counter# now we mix the role in 
$toc-counter.inc(1).inc(2).inc(2).inc(1).inc(2).inc(2).inc(3).inc(3);
put $toc-counter / 1;          # OUTPUT: «NaN␤» (because that's numerical context) 
put $toc-counter;              # OUTPUT: «2.2.2␤» (put will call TOC-Counter::Str) 

Roles can be anonymous.

my %seen of Int is default(0 but role :: { method Str() {'NULL'} });
say %seen<not-there>;          # OUTPUT: «NULL␤» 
say %seen<not-there>.defined;  # OUTPUT: «True␤» (0 may be False but is well defined) 
say Int.new(%seen<not-there>); # OUTPUT: «0␤»