Parametric roles, and what role Foo[::T] means
Every snippet on this page was run on both Raku++ and Rakudo. They give identical output except in one place, which is shown with both outputs.
role LinearArray[::T] { ... }
This is a parametric role: a role that takes arguments, much like a generic in C++, Java or Rust. The square brackets are the role's parameter list. They work like a routine's signature, but the arguments are bound when the role is applied to a class, not when something is called.
What is ::T? #
It is a type capture. Whatever type is passed in gets the name T, and inside the role T can go anywhere a type name can go: on an attribute, on a parameter, or as a type object you call methods on.
role LinearArray[::T] {
has T @.items;
method push(T $x) { @!items.push($x); self }
method of-type { T.^name }
}
class IntList does LinearArray[Int] { }
my $l = IntList.new;
$l.push(1).push(2);
say $l.items; # [1 2]
say $l.of-type; # Int
The type is enforced. push on an IntList takes an Int, so anything else fails to bind:
role LinearArray[::T] {
method push(T $x) { say "pushed $x" }
}
class IntList does LinearArray[Int] { }
IntList.new.push("three");
CATCH { default { say .^name } } # X::TypeCheck::Binding::Parameter
Type captures are not special to roles. A sub can capture the type of one argument and require it of another:
sub same-type(::T $a, T $b) { "both are {T.^name}" }
say same-type(1, 2); # both are Int
say same-type('a', 'b'); # both are Str
say same-type(1, 'b'); # dies: X::TypeCheck::Binding::Parameter
Do I need a class to use one? #
No. Calling .new on a role puns it: Raku makes an anonymous class that does the role and instantiates that. This works for a parametrised role too:
role LinearArray[::T] {
has T @.items;
method of-type { T.^name }
}
my $s = LinearArray[Str].new(items => <a b c>);
say $s.items; # [a b c]
say $s.of-type; # Str
Several parameters, and defaults #
The brackets take a full signature, so you can have more than one parameter, and defaults work as they do in a sub:
role Pairing[::K, ::V = Str] {
method kinds { K.^name ~ ' => ' ~ V.^name }
}
say Pairing[Int].new.kinds; # Int => Str
say Pairing[Int, Num].new.kinds; # Int => Num
The parameters do not have to be types #
Any value can be a parameter. Here the role is parametrised by a size:
role Ring[Int $size] {
has @.slots = Any xx $size;
method size { $size }
}
my $r = Ring[4].new;
say $r.size; # 4
say $r.slots.elems; # 4
Can I overload a role on its parameters? #
Yes. Declare the role more than once with different signatures, and Raku picks the variant by multiple dispatch on the arguments, the same way it picks a multi candidate:
role Describe[Int $n] { method what { "an integer, $n" } }
role Describe[Str $s] { method what { "a string, $s" } }
say Describe[42].new.what; # an integer, 42
say Describe['hi'].new.what; # a string, hi
Checking what something does #
Smartmatching against the bare role name matches any parametrisation. With arguments, it matches only that one:
role LinearArray[::T] { }
class IntList does LinearArray[Int] { }
say IntList ~~ LinearArray; # True
say IntList ~~ LinearArray[Int]; # True
say IntList ~~ LinearArray[Str]; # False
Where you have already met them #
Typed arrays and hashes are parametric types too. my Int @a declares an Array[Int], and you can name that type directly:
my Int @a = 1, 2, 3;
say @a.^name; # Array[Int]
say @a ~~ Array[Int]; # True
my %h{Str} of Int;
say %h.^name; # Hash[Int,Str]
say Array[Str].new(<x y>); # [x y]
Hash[Int,Str] names the value type first and the key type second, the same order as of Int and {Str} in the declaration.
What if I leave the arguments off? #
A role whose parameter has no default needs an argument. Rakudo rejects does LinearArray at compile time:
role LinearArray[::T] { has T @.items }
class Plain does LinearArray { }
say Plain.new.items.^name;
# Rakudo: ===SORRY!=== No appropriate parametric role variant available
# for 'LinearArray'
# Raku++: Array[T]
Raku++ accepts the program and leaves T unbound, which is not something to rely on. If a role should work without an argument, give the parameter a default, and then the two engines agree:
role LinearArray[::T = Any] { has T @.items }
class Plain does LinearArray { }
say Plain.new.items.^name; # Array[Any]