Apocalypse 12
by Larry Wall
|
Pages: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
Editor's Note: this Apocalypse is out of date and remains here for historic reasons. See Synopsis 12 for the latest information.
Use of Roles at Runtime (mixins)
While roles are at their most powerful at compile time, they can also function as mixin classes at runtime. The "does" binary operator performs the feat of deriving a new class and binding the object to it:
$fido does Sentry
Actually, it only does this if $fido doesn't already do
the Sentry role. If it does already, this is basically
a no-op. The does operator works on the object in place.
It would be illegal to say, for instance,
0 does true
The does operator returns the object so you can nest mixins:
$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.
(Do not confuse the binary does with the unary does
that you use inside a class definition to pull in a role.)
In contrast to does, the but operator works
on a copy. So you can say:
0 but true
and you get a mixin based on a copy of 0, not the original 0, which everyone shares. One other wrinkle is that "true" isn't, in fact, a class name. It's an enumerated value of a bit class. So what we said was a shorthand for something like:
0 but bit::true
In earlier Apocalypses we talked about applying properties with but.
This has now been unified with mixins, so any time you say:
$value but prop($x)
you're really doing something more like
$tmp = $value; # make a copy
$tmp does SomeRole; # guarantee there's a rw .prop method
$tmp.prop = $x; # set the prop method
And therefore a property is defined by a role like this:
role SomeRole {
has SomeType $.prop is rw = 1;
}
This means that when you mention "prop" in your program,
something has to know how to map that to the SomeRole role.
That would often be something like an enum declaration. It's illegal
to use an undeclared property. But sometimes you just want a random
old property for which the role has the same name as the property. You
can declare one with
my property answer;
and that essentially declares a role that looks something like
my role answer {
has $.answer is rw = 1;
}
Then you can say
$a = 0 but answer(42)
and you have an object of an anonymous type that "does" answer,
and that include a .answer accessor of the same name, so
that if you call $a.answer, you'll get back 42.
But $a itself has the value 0. Since the accessor
is "rw", you can also say
$a.answer = 43;
There's a corresponding assignment operator:
$a but= tainted;
That avoids copying $a before tainting it. It basically
means the same thing as
$a does taint::tainted
For more on enumerated types, see Enums below.
Traits
Here we're talking about Perl's traits (as in compile-time properties), not Traits (as in the Traits paper).
Traits can be thought of as roles gone wrong. Like roles, they can function as straightforward mixins on container objects at compile time, but they can also cheat, and frequently do. Unlike roles, traits are not constrained to play fair with each other. With traits, it's both "first come, first served", and "he who laughs last laughs best". Traits are applied one at a time to their container victim, er, object, and an earlier trait can throw away information required by a later trait. Contrariwise, a later trait can overrule anything done by an earlier trait--except of course that it can't undestroy information that has been totally forgotten by the earlier trait.
You might say that "role" is short for "role model", while "trait" is short for "traitor". In a nutshell, roles are symbiotes, while traits are parasites. Nevertheless, some parasites are symbiotic, and some symbiotes are parasitic. Go figure...
All that being said, well-behaved traits are really just roles applied to declared items like containers or classes. It's the declaration of the item itself that makes traits seem more permanent than ordinary properties. The only reason we call them "traits" rather than "properties" is to continually remind people that they are, in fact, applied at compile time. (Well, and so that we can make bad puns on "traitor".)
Even ill-behaved traits should add an appropriately named role to the container, however, in case someone wants to look at the metadata properties of the container.
Traits are generally inflicted upon the "traitee" with the "is" keyword, though other modalities are possible. When the compiler sees words like "is" or "will" or "returns" or "handles", or special constructs like signatures and body closures, it calls into an associated trait handler, which applies the role to the item as a mixin, and also does any other traitorous magic that needs doing.
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 sub trait_auxiliary:is(xxx $trait, Class $container: ?$arg) {...}
multi sub trait_auxiliary:is(xxx $trait, Any $container: ?$arg) {...}
}
Then it can function as a trait. A well-behaved trait handler will say
$container does xxx($arg);
somewhere inside to set the metadata on the container correctly. Then not only can you say
class MyClass is xxx(123) {...}
but you'll also be able to say
if MyClass.meta.xxx == 123 {...}
Since a class can function as a role when it comes to parameter type matching, you can also say:
class MyBase {
multi sub trait_auxiliary:is(MyBase $base, Class $class: ?$arg) {...}
multi sub trait_auxiliary:is(MyBase $tied, Any $container: ?$arg) {...}
}
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 sub *trait_auxiliary:is(Class $base, Class $class: ?$arg) {...}
which adds $base to the "isa" list of $class,
or
multi sub *trait_auxiliary:is(Class $tied, Any $container: ?$arg) {...}
which sets the "tie" type of the container to the implementation type
in $tied.
In any event, if the trait supplies the optional argument, that comes
in as $arg. (It's probably something unimportant, like
the function body...) Note that unlike "pair options" such as ":wag",
traits do not necessarily default to the value 1 if you don't supply
the argument. This is consistent with the notion that traits don't generally
do something passive like setting a value somewhere, but something active
like totally screwing up the structure of your container.
Most traits are introduced by use of a "helping verb", which could be
something like "is", or "will", or "can",
or "might", or "should", or "does".
We call these helping verbs "trait auxiliaries". Here's "will",
which (being syntactic sugar) merely delegates to back to "is":
multi sub *trait_auxiliary:will($trait, $container: &arg) {
trait_auxiliary:is($trait, $container, &arg);
}
Note the declaration of the argument as a non-optional reference to a closure. This is what allows us to say:
my $dog will eat { anything() };
rather than having to use parens:
my $dog is eat({ anything() });
Other traits are applied with a single word, and we call one of those
a "trait verb". For instance, the "returns" trait described
in Apocalypse 6 is defined something like this:
role returns {
has ReturnType $.returns;
multi sub trait_verb:returns($container: ReturnType $arg) {
$container does returns($arg);
}
...
}
Note that the argument is not optional on "returns".
Earlier we defined the xxx trait using multi sub definitions:
role xxx {
has Int $.xxx;
multi sub trait_auxiliary:is(xxx $trait, Class $container: ?$arg) {...}
multi sub trait_auxiliary:is(xxx $trait, Any $container: ?$arg) {...}
}
This is one of those situations in which you may really want single-dispatch methods:
role xxx {
has Int $.xxx;
method trait_auxiliary:is(xxx $trait: Class $container, ?$arg) {...}
method trait_auxiliary:is(xxx $trait: Any $container, ?$arg) {...}
}
Some traits are control freaks, so they want to make sure that anything mentioning them comes through their control. They don't want something dispatching to another trait's trait_help:is method just because someone introduced a cute new container type they don't know about. That other trait would just mess things up.
Of course, if a trait is feeling magnanimous, it should just go ahead and use multi subs. Since the multi-dispatcher takes into account single-dispatch methods, and the distance of an exact match on the first argument is 0, the dispatcher will generally respect the wishes of both the paranoid and the carefree.
Note that we included "does" in our list of "helping verbs". Roles actually
implement themselves using the trait interface, but the generic version
of trait_auxiliary:does defaults to doing proper roley
things rather than proper classy things or improper traitorous things.
So yes, you could define your own trait_auxiliary:does
and turn your nice role traitorous. That would be...naughty.
But apart from how you typically invoke them, traits and roles are really
the same thing. Just like the roles on which they're based, you may
neither instantiate nor inherit from a trait. You may, however, use
their names as type constraints on multimethod signatures and such.
As with well-behaved roles, they should define attributes or methods
that show up as metadata properties where that's appropriate. 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 a container at runtime, but if you do, it's just an
ordinary mixin role. You have to call the appropriate trait_auxiliary:is()
routine yourself if you want it to do any extra shenanigans. The compiler
won't call it for you at runtime like it would at compile time.
When you define a helping verb such as "is" or "does", it not only makes
it a postfix operator for declarations, but a unary operator within
class and role closures. Likewise, declarative closure blocks like BEGIN
and INIT are actually trait verbs, albeit ones that can
add multiple closures to a queue rather than adding a single property.
This implies that something like
sub foo {
LEAVE {...}
...
}
could (except for scoping issues) equivalently be written:
sub foo LEAVE {...} {
...
}
Though why you'd want to that, I don't know. Hmm, if we really generalize trait verbs like that, then you could also write things like:
sub foo {
is signature ('int $x');
is cached;
returns Int;
...
}
That's getting a little out there. Maybe we won't generalize it quite that far...
Pages: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 |

