Skip to content

Latest commit

 

History

History
611 lines (443 loc) · 21.1 KB

S14-roles-and-parametric-types.pod

File metadata and controls

611 lines (443 loc) · 21.1 KB

TITLE

Synopsis 14: Roles and Parametric Types [DRAFT]

VERSION

Created: 24 Feb 2009 (extracted from S12-objects.pod)

Last Modified: 24 Nov 2012
Version: 11

Overview

This synopsis discusses roles and parametric types, which were originally discussed in A12.

Roles

Classes are primarily in charge of object management, and only secondarily in charge of software reuse. In Perl 6, roles take over the job of managing software reuse. Depending on how you care to look at it, a role is like a partial class, or an interface with default implementation, or a set of generic methods and their associated data, or a class closed at compile time.

Roles may be composed into a class at compile time, in which case you get automatic detection of conflicting methods. A role may also be mixed into a class or object at run time to produce an anonymous derived class with extra capabilities, but in this case conflicting methods are overridden by the new role silently. In either case, a class is necessary for instantiation--a role may not be directly instantiated.

A role is declared like a class, but with a role keyword:

role Pet {
    method feed ($food) {
        $food.open_can;
        $food.put_in_bowl;
        self.eat($food);
    }
}

A role may not inherit from a class, but may be composed of other roles. However, this "crony" composition is not evaluated until class composition time. This means that if two roles bring in the same crony, there's no conflict--it's just as if the class pulled in the crony role itself and the respective roles didn't. A role may never conflict with itself regardless of its method of incorporation. A role that brings in two conflicting crony roles may resolve them as if it were a class. This solution is accepted by the class unless the class supplies its own solution. If two different roles resolve the same crony conflict two different ways, those roles are themselves in conflict and must be resolved by a "more derived" role or the class.

A role doesn't know its own type until it is composed into a class. Any mention of its main type (such as ::?CLASS) is generic, as is any reference to self or the type of the invocant. You can use a role name as a type, but only for constraints, not for declaring actual objects. (However, if you use a role as if it were a class, an anonymous class is generated that composes the role, which provides a way to force a role to test its crony composition for infelicities.)

If a role merely declares methods without defining them, it degenerates to an interface:

role Pet {
    method feed ($food) {...}
    method groom () {...}
    method scratch (:$where) {...}
}

Note that, while these methods must become available at class composition time, they might be supplied by any of: another role, the class itself, or some superclass. We know the methods that are coming from the other roles or the class, but we don't necessarily know the complete set of methods supplied by our super classes if they are open or rely on wildcard delegation. However, the class composer is allowed to assume that only currently declared superclass methods or non-wildcard methods are going to be available. A stub can always be installed somewhere to "supply" a missing method's declaration.

Roles may have attributes:

role Pet {
    has $.collar = Collar.new(tag => Tag.new);
    method id () { return $.collar.tag }
    method lose_collar () { undefine $.collar }
}

Within a role the has declarator always indicates the declaration from the viewpoint of the class. Therefore a private attribute declared using has is private to the class, not to the role. You may wish to declare an attribute that is hidden even from the class; a completely private role attribute (that will exist per instance of the class) may be declared like this:

my $!spleen;

The name of such a private attribute is always considered lexically scoped. If a role declares private lexical items, those items are private to the role due to the nature of lexical scoping. Accessors to such items may be exported to the class, but this is not the default. In particular, a role may say

trusts ::?Class;

to allow self!attr() access to the role's $!attr variables with the class or from other roles composed into the class. Conflicts between private accessors are also caught at composition time, but of course need not consider super classes, since no-one outside the current class (or a trusted class) can call a private accessor at all. (Private accessors are never virtual, and must be package qualified if called from a trusted scope other than our own. That is, it's either self!attr() or $obj!TrustsMe::attr().)

A role may also distinguish a shared method

has method foo ...
method foo ...      # same

from a nonshared private method:

my method !foo ...
my method foo ...   # same, but &foo is aliased to &!foo

Generally you'd just use a lexically scoped sub, though.

my sub foo ...

A role can abstract the decision to delegate:

role Pet {
    has $groomer handles <bathe groom trim> = hire_groomer();
}

Note that this puts the three methods into the class as well as $groomer. In contrast, "my $!groomer" would only put the three methods; the attribute itself is private to the role.

A role is allowed to declare an additional inheritance for its class when that is considered an implementation detail:

role Pet {
    also is Friend;
}

Compile-time Composition

A class incorporates a role with the verb "does", like this:

class Dog is Mammal does Pet does Sentry {...}

or equivalently, within the body of the class closure:

class Dog {
    also is Mammal;
    also does Pet;
    also does Sentry;
    ...
}

or

class Dog {
    also is Mammal does Pet does Sentry;
    ...
}

There is no ordering dependency among the roles.

A class's explicit method definition hides any role definition of the same name. A role method in turn hides any methods inherited from other classes.

If there are no method name conflicts between roles (or with the class), then each role's methods can be installed in the class. If, however, two roles try to introduce a method of the same name the composition of the class fails. (Two has attributes of the same name, whether public or private, are always a composition fail. Role-private attributes are exempt from this, and from the viewpoint of the composition, don't even exist, except to allocate a slot for each such attribute.)

There are several ways to solve method conflicts. The first is simply to write a class method that overrides the conflicting role methods, perhaps figuring out which role method to call.

Alternately, if the role's methods are declared multi, they can be disambiguated based on their long name. If the roles forget to declare them as multi, you can force a multi on the roles' methods by installing a proto stub in the class being constructed:

proto method shake {...}

(This declaration need not precede the does clause textually, since roles are not actually composed until the end of the class definition, at which point we know which roles are to be composed together in a single logical operation, as well as how the class intends to override the roles.)

The proto method will be called if the multi fails:

proto method shake { warn "They couldn't decide" }

Run-time Mixins

Run-time mixins are done with does and but. The does binary operator is a mutator that derives a new anonymous class (if necessary) and binds the object to it:

$fido does Sentry

The does infix operator is non-associative, so this is a syntax error:

$fido does Sentry does Tricks does TailChasing does Scratch;

You can, however, say

$fido does Sentry;
$fido does Tricks;
$fido does TailChasing;
$fido does Scratch;

And since it returns the left side, you can also say:

((($fido does Sentry) does Tricks) does TailChasing) does Scratch;

Unlike the compile-time role composition, each of these layers on a new mixin with a new level of inheritance, creating a new anonymous class for dear old Fido, so that a .chase method from TailChasing hides a .chase method from Sentry.

You can also mixin a precomposed set of roles:

$fido does (Sentry, Tricks, TailChasing, Scratch);

This will level the playing field for collisions among the new set of roles, and guarantees the creation of no more than one more anonymous class. Such a role still can't conflict with itself, but it can hide its previous methods in the parent class, and the calculation of what conflicts is done again for the set of roles being mixed in. If you can't do compile-time composition, we strongly recommend this approach for run-time mixins since it approximates a compile-time composition at least for the new roles involved.

A role applied with does may be parameterized with an initializer in parentheses, but only if the role supplies exactly one attribute to the mixin class:

$fido does Wag($tail);
$line does taint($istainted);

Note that the parenthesized form is not a subroutine or method call. It's just special initializing syntax for roles that contain a single property.

The supplied initializer will be coerced to the type of the attribute. Note that this initializer is in addition to any parametric type supplied in square brackets, which is considered part of the actual type name:

$myobj does Array[Int](@initial)

A property is defined by a role like this:

role answer {
    has Int $.answer is rw = 1;
}

The property can then be mixed in or, alternatively, applied using the but operator. but is like does, but creates a copy and mixes into that instead, leaving the original unmodified. Thus:

$a = 0 but answer(42)

Really means something like:

$a = ($anonymous = 0) does answer(42);

Which really means:

(($anonymous = 0) does answer).answer = 42;
$a = $anonymous;

Which is why there's a but operator.

If you put something that is not a role on the right hand side of the does or but operators then an anonymous role will be auto-generated containing a single method that returns that value. The name of the method is determined by doing .WHAT.perl on the value supplied on the RHS. The generated role is then mixed in to the object. For example:

$x does 42

Is equivalent to:

$x does role { method Int() { return 42 } }

Note that the role has no attributes and thus no storage; if you want that, then you should instead use:

$x does Int(42)

Which mixes in the Int role and initializes the single storage location Int that it declares with 42, and provides an lvalue accessor.

Note that .WHAT on an enumeration value stringifies to the name of the enumeration, and as a result:

0 but True

Is equivalent to:

0 but role { method Bool() { return True } }

And thus the resulting value will be considered true in boolean context.

The list syntax for composing multiple roles in a single does or but by putting them in a list also applies here. Thus:

42 but ("the answer", False)

Is equivalent to:

42 but (role { method Str() { return "the answer" } },
        role { method Bool() { return False } })

Which gives you a compact way to build context-sensitive return values. Note that multiple roles rather than a single one are generated, so that anything like:

42 but (True, False)

Will fail as a result of standard role composition semantics (because two roles are both trying to provide a method Bool).

Traits

Traits are just properties (roles) applied to something that is being declared (the declarand), such as containers or classes. It's the declaration of the item itself that makes traits seem more permanent than ordinary properties. In addition to adding the property, a trait can also have side effects.

Traits are generally applied with the "is" keyword, though not always. To define a trait handler for an "is xxx" trait, define one or more multisubs into a property role like this:

role xxx {
    has Int $.xxx;
    multi trait_mod:<is>(::?CLASS:U $declarand, :$xxx!) {...}
    multi trait_mod:<is>(Any $declarand, :$xxx!) {...}
}

Then it can function as a trait. A well-behaved trait handler will say

$declarand does xxx($arg);

somewhere inside to set the metadata on the declarand correctly. Since a class can function as a role when it comes to parameter type matching, you can also say:

class MyBase {
    multi trait_mod:<is>(MyBase:U $declarand, MyBase $base) {...}
    multi trait_mod:<is>(Any $declarand, MyBase $tied) {...}
}

These capture control if MyBase wants to capture control of how it gets used by any class or container. But usually you can just let it call the generic defaults:

multi trait_mod:<is>(Any:U $declarand, $base) {...}

which adds $base to the "isa" list of class $declarand, or

multi trait_mod:<is>(Any $declarand, $tied) {...}

which sets the "tie" type of the container declarand to the implementation type in $tied.

Most traits are really just adverbial pairs which, instead of being introduced by a colon, are introduced by a (hopefully) more readable "helping verb", which could be something like "is", or "will", or "can", or "might", or "should", or "does". Any trait verb that is parsed the same as trait_mod:<is> may be defined the same way. Here's "will", which (being syntactic sugar) merely delegates to back to "is":

multi sub trait_mod:<will>($declarand, :$trait) {
    trait_mod:<is>($declarand, :$trait);
}

Other traits are applied with a single word, and require special parsing. For instance, the "of" trait is defined something like this:

role of {
    has ReturnType $.of;
    multi sub trait_mod:<of>($declarand, ReturnType $arg) is parsed /<typename>/ {
        $declarand does of($arg);
    }
    ...
}

Unlike compile-time roles, which all flatten out in the same class, compile-time traits are applied one at a time, like mixin roles. You can, in fact, apply a trait to an object at run time, but if you do, it's just an ordinary mixin role. You have to call the appropriate trait_mod:<is()> routine yourself if you want it to do any extra shenanigans. The compiler won't call it for you at run time like it would at compile time.

Note that the declarations above are insufficient to install new trait auxiliaries or verbs into the user's grammar, since macro definitions are lexically scoped, and in the declarations above extend only to the end of the role definition. The user's lexical scope must somehow have processed (or imported) a proto declaration introducing the new syntax before it can be parsed correctly. (This doesn't apply to pre-existing syntax such as is, of course.)

Calls to trait_mod routines are evaluated in sink context.

Parametric Roles

A role's main type is generic by default, but you can also parameterize other types explicitly using type parameters:

role Pet[::Petfood = TableScraps] {
    method feed (Petfood $food) {...}
}

(Note that in this case you must not use ::Petfood in the inner declaration, or it would rebind the type to type of the actual food parameter.)

If you want to parameterize the initial value of a role attribute, be sure to put a double semicolon if you don't want the parameter to be considered part of the long name:

role Pet[::ID;; $tag] {
    has ID $.collar .= new($tag);
}

You don't just have to parameterize on types; any value is fine. Imagine we wanted to factor out a "greet" method into a role, which takes somebody's name and greets them. We can parameterize it on the greeting.

role Greet[Str $greeting] {
    method greet() { say "$greeting!"; }
}
class EnglishMan does Greet["Hello"] { }
class Slovak does Greet["Ahoj"] { }
class Lolcat does Greet["OH HAI"] { }
EnglishMan.new.greet(); # Hello
Slovak.new.greet(); # Ahoj
Lolcat.new.greet(); # OH HAI

Similarly, we could do a role for requests.

role Request[Str $statement] {
    method request($object) { say "$statement $object?"; }
}
class EnglishMan does Request["Please can I have a"] { }
class Slovak does Request["Prosim si"] { }
class Lolcat does Request["I CAN HAZ"] { }
EnglishMan.new.request("yorkshire pudding");
Slovak.new.request("borovicka");
Lolcat.new.request("CHEEZEBURGER");

Sadly, the Slovak output sucks here. Borovicka is the nominative form of the word, and we need to decline it into the accusative case. But some languages don't care about that, and we don't want to have to make them all supply a transform. Thankfully, you can write many roles with the same short name, and a different signature, and multi-dispatch will pick the right one for you (it is the exact same dispatch algorithm used by multi-subs). So we can write:

role Request[Str $statement] {
    method request($object) { say "$statement $object?"; }
}
role Request[Str $statement, &transform] {
    method request($object) {
        say "$statement " ~ transform($object) ~ "?";
    }
}
module Language::Slovak {
    sub accusative($nom) {
        # ...and before some smartass points it out, I know
        # I'm missing some of the masculine animate declension...
        return $nom.subst(/a$/, 'u');
    }
}
class EnglishMan does Request["Please can I have a"] { }
class Slovak does Request["Prosim si", &Language::Slovak::accusative] { }
class Lolcat does Request["I CAN HAZ"] { }
EnglishMan.new.request("yorkshire pudding");
Slovak.new.request("borovicka");
Lolcat.new.request("CHEEZEBURGER");

Which means we can now properly order our borovicka in Slovakia, which is awesome. Until you do it in a loop and find the Headache['very bad'] role got mixed into yourself overnight, anyway...

Relationship Between of And Types

The of keyword is just syntactic sugar for providing a single parameter to a parametric type. Thus:

my Array of Recipe %book;

Actually means:

my Array[Recipe] %book;

This can be nested, so:

my Hash of Array of Recipe @library;

Is just:

my Hash[Array[Recipe]] @library;

Therefore:

my Array @array;

Means an Array of Array (actually, a Positional of Array).

Parametric Subtyping

If you have two types in a subtyping relationship such that T1 is narrower than T2, then also the roles:

role R[::T] { }
role R[::T1, ::T2] { }

Will act such that R[T1] is narrower than R[T2]. This extends to multiple parameters, however they must all be narrower or the same (this is unlike in multiple dispatch where you can have one narrower and the rest narrower or tied). That is, assuming we have some unrelated type T3, then R[T2, T1] is narrower than R[T1,T1] but R[T2,T1] is not narrower than R[T1,T3].

Nesting follows naturally from this definition, so a role R[R[T2]] is narrower than a role R[R[T1]].

This all means that, for example, if you have a sub:

sub f(Num @arr) { ... }

Then you can also call it with an array of Int.

my Int @a = 1,2,3;
f(@a);

The structure of role types and packages

When a role is declared:

role Foo { }

Then the symbol `Foo` that gets installed in the enclosing package represents a potential group of roles that will be disambiguated by arguments. A later declaration:

role Foo[::T] { }

Adds to the existing group. The ::?PACKAGE inside of each of these is therefore something different from Foo itself. The group's meta-object type is ParametricRoleGroupHOW, while ParametricRoleHOW is the meta-object type for individual parametric roles.

Writing Foo[Int] is not sufficient to concretize a role, because that needs the target class; therefore this returns a type with the meta-object CurriedRoleHOW that will pass along the extra type parameters at the point of composition, to aid selection of the correct parametric variant. Therefore, it does not address a particular ParametricRoleHOW.

Normal users of Perl 6 will have to care little for these details. However, given that any our scoped symbol declared inside of a role body will end up in a package that is simply unaddressable without digging into the meta-objects, our declarations in a role are forbidden. This does not prevent installation directly into ::?PACKAGE.WHO, for expert users who want the escape hatch. It simply means syntactic our declarations will be forbidden to avoid the inevitable confusion that will result from trying to access them.

Interaction of typed and untyped data structures

Certainly so far as Perl 6.0.0 goes, only types that have been declared on a container count in the type check. That is, if we have a sub:

sub f(Int @arr) { ... }

And call it with any of:

f([1,2,3]);
my @a = 1,2,3;
f(@a);

Then neither of these calls will work. The type check is based on the declared type of the array, and the content is unknown to the type checker.

AUTHORS

Larry Wall <[email protected]>
Tim Nelson <[email protected]>
Jonathan Worthington <[email protected]>