2020/12/21

2020 Perl Advent Calendar - Day 21

<< First | < Prev | Next >

So far we've been looking at features of some syntax modules that are relatively well-established - Future::AsyncAwait has a couple of years of production battle-testing against it, and even Object::Pad's basic class features have been found to be quite stable over the past six months or so. For today's article I'd like to take a slightly different direction and take a look at something much newer and still under experimental design.

Some object systems which use inheritance to create derived classes out of base ones (including the base system in Perl itself) support the idea that a given class may have multiple bases. This is called Multiple Inheritance. Iniitally it may sound like a useful feature to have, but in practice trying to support it makes implementations of object systems more complicated, and can lead to situations where the choice of correct behaviour is non-obvious, or in some cases conflicting with what may seem sensible. Situations get especially complicated if the same partial class appears multiple times in the inheritance hierarchy leading up to a given class.

For this reason most modern object systems, including Object::Pad, do not support multiple interitance, to keep behaviours simpler. In order to try to provide the same useful properties (that of being able to share code from multiple component classes), they provide a somewhat different idea, called roles. A role can be considered similar to a partial class which can be merged into a real class. A role can provide methods, BUILD blocks, and slot variables. In many ways a role appears the same as a class, except that instances of it cannot be directly created. To be used as an instance a role must be applied to a class. This has the effect of copying all of the pieces of that role into the target class.

For example, in the Tickit-Widget-Menu distribution there are two different classes of object that can appear in a menu - an individual menu item, or a submenu. In order to avoid code duplication by copying parts of the implementation around both classes, the common behaviours are implemented in a role, by using the role keyword:

use Object::Pad 0.33;

role Tickit::Widget::Menu::itembase;

has $_name;

BUILD (%args)
{
    $_name = $args{name}
}

...

To apply this role to both of the required classes each uses the implements keyword on its class statement to copy the components of that role into the class:

use Object::Pad 0.33;

class Tickit::Widget::Menu:::Item
    implements Tickit::Widget::Menu::itembase;
...

class Tickit::Widget::Menu::base
    implements Tickit::Widget::Menu::itembase;
...

Superficially this might feel like it suffers the same problems as multiple inheritance, but keep in mind that applying a role is basically just a fancy form of copy-pasting the code into the class. There is no runtime lookup of methods or other class items whenever they are accessed. The parts of a role are simply copied individually into the class that applies it. This means that any naming conflicts are detected as errors at compile-time, alerting the programmer to the potential problem:

use Object::Pad 0.33;

role R
{
    method collides() {}
}

class C implements R
{
    method collides() {}
}
$ perl example.pl
Method 'collides' clashes with the one provided by role R at ...

A program will only successfully compile if there are no naming collisions. As a result of this, and because the pieces of the role are simply copied into a class, it means that it does not matter in what order individual roles are applied to a class, nor does it matter if the same role is applied multiple times within the hierarchy (e.g. if both a class and its base class tried to apply the same role). The end result is always the same, presuming no conflicts. This compiletime check, and flexibility on ordering and duplicate application, helps to ensure more robust code.

<< First | < Prev | Next >

No comments:

Post a Comment