Sign In/My Account | View Cart  
advertisement


Listen Print

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

Next Pagearrow