syntax role
1 | language documentation Object orientation |
1.1 | (Object orientation) declarator role role |
1.2 | Applying roles |
1.3 | Stubs |
1.4 | Inheritance |
1.5 | Pecking order |
1.6 | Automatic role punning |
1.7 | Parameterized roles |
1.8 | Mixins of roles |
Documentation for syntax role
assembled from the following types:
language documentation Object orientation
From Object orientation
(Object orientation) declarator role role
Roles are a collection of attributes and methods; however, unlike classes, roles are meant for describing only parts of an object's behavior; this is why, in general, roles are intended to be mixed in classes and objects. In general, classes are meant for managing objects and roles are meant for managing behavior and code reuse within objects.
Roles use the keyword role
preceding the name of the role that is declared. Roles are mixed in using the does
keyword preceding the name of the role that is mixed in.
Roles can also be mixed into a class using is
. However, the semantics of is
with a role are quite different from those offered by does
. With is
, a class is punned from the role, and then inherited from. Thus, there is no flattening composition, and none of the safeties which does
provides.
constant ⲧ = " " xx 4; #Just a ⲧabdoes Notablemy = Journey.new( :origin<Here>, :destination<There>,travelers => <þor Freya> );.notes("First steps");notes : "Almost there";print ;# OUTPUT:#⤷ Here# First steps# Almost there##There ⤶
Roles are immutable as soon as the compiler parses the closing curly brace of the role declaration.
Applying roles
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.
is Bull is Automobilemy = Taurus.new;say .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:
does Bull-Like does Steerable
This code will die with something like:
===SORRY!===Method 'steer' must be resolved by because it exists inmultiple roles (Steerable, Bull-Like)
This check will save you a lot of headaches:
does Bull-Like does Steerable
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
does R1does R2
produces the same class C
as
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.
# the following is a compile time error, for example# Method 'serialize' must be implemented by Point because# it's required by a roledoes AbstractSerializable# this works:does AbstractSerializable
The implementation of the stubbed method may also be provided by another role.
Inheritance
Roles cannot inherit from classes, but they may carry classes, causing any class which does that role to inherit from the carried classes. So if you write:
is Exceptiondoes AX::Ouch.^parents.say # OUTPUT: «((Exception))»
then X::Ouch
will inherit directly from Exception, as we can see above by listing its parents.
As they do not use what can properly be called inheritance, roles are not part of the class hierarchy. Roles are listed with the .^roles
metamethod instead, which uses transitive
as flag for including all levels or just the first one. Despite this, a class or instance may still be tested with smartmatches or type constraints to see if it does a role.
does FG.^roles.say; # OUTPUT: «((F))»does Urdoes Ar ; Whim.^roles(:!transitive).say; # OUTPUT: «((Ar))»say G ~~ F; # OUTPUT: «True»multi a (F )multi a ()a(G); # OUTPUT: «F»
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.
is A does Mis A does MB.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 or use it as a type object will automatically create a class with the same name as the role, making it possible to transparently use a role as if it were a class.
say Point.new(x => 6, y => 8).abs; # OUTPUT «10»say Point.dimensions; # OUTPUT «2»
We call this automatic creation of classes punning, and the generated class a pun.
Punning is not caused by most metaprogramming constructs, however, as those are sometimes used to work directly with roles.
Parameterized roles
Roles can be parameterized, by giving them a signature in square brackets:
[::Type]my = BinaryTree[Int].new-from-list(4, 5, 6);.visit-preorder(); # OUTPUT: «546».visit-postorder(); # OUTPUT: «465»
Here the signature consists only of a type capture, but any signature will do:
<debug info warn error critical>;[ = ]Logging[].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.
;my = 2 but R;sub f(\bound);f(); # OUTPUT: «hidden!»my := <a b> but R;say .^name; # OUTPUT: «List+{R}»
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 as is shown in the example with @positional
. Some operators will return a new value, which effectively strips the mixin from the result. That is why it might be more clear to mix in the role in the declaration of the variable using does
:
;my does R = <a b>;say .^name; # OUTPUT: «Array+{R}»
The operator infix:<but>
is narrower than the list constructor. When providing a list of roles to mix in, always use parentheses.
my = 1 but R1,R2; # R2 is in sink context, issues a WARNINGsay .^name;# OUTPUT: «Int+{R1}»my = 1 but (R1,R2);say .^name; # OUTPUT: «Int+{R1,R2}»
Mixins can be used at any point in your object's life.
# A counter for Table of Contentsmy Num = NaN; # don't do math with Not A Numbersay ; # OUTPUT: «NaN»does TOC-Counter; # now we mix the role in.inc(1).inc(2).inc(2).inc(1).inc(2).inc(2).inc(3).inc(3);put / 1; # OUTPUT: «NaN» (because that's numerical context)put ; # OUTPUT: «2.2.2» (put will call TOC-Counter::Str)
Roles can be anonymous.
my of Int is default(0 but role :: );say <not-there>; # OUTPUT: «NULL»say <not-there>.defined; # OUTPUT: «True» (0 may be False but is well defined)say Int.new(<not-there>); # OUTPUT: «0»