Documentation for declarator role, assembled from the following types:
From Object Orientation
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;does Serializablemy = Point.new(:x(1), :y(2));my = .serialize;my = Point.deserialize();say .x; # OUTPUT: «1␤»
Roles are immutable as soon as the compiler parses the closing curly brace of the role declaration.
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
does R1 does R2
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.
Roles cannot inherit from classes, but they may cause any class which does that role to inherit from another class. So if you write:
is Exceptiondoes AX::Ouch.^parents.say # OUTPUT: «((Exception))␤»
X::Ouch will inherit directly from Exception, as we can see above by listing its parents.
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.
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.
say Point.new(x => 6, y => 8).abs; # OUTPUT «10␤»
We call this automatic creation of classes punning, and the generated class a pun.
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: «5␤4␤6␤».visit-postorder(); # OUTPUT: «4␤6␤5␤»
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!␤»
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 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␤»