RFC 265: Interface polymorphism considered lovely

A little preface with an off-topic first. In the process of writing this post I was struck by the worst sysadmin’s nightmare: loss of servers followed by a bad backup. Until the very last moment I have had well-grounded fears of not finishing the post whatsoever. Luckily, I made a truce with life to get temporary respite. A conclusion? Don’t use bareos with ESXi. Or, probably, just don’t use bareos…

While picking up a RFC for my previous advent post I was totally focused on language-objects section. It took me a few passes to find the right one to cover. But in the meantime I realized that a very important topic is actually missing from the list. “Impossible!” – I said to myself and went onto another hunt later. Yet, neither search for “abstract class”, nor for “role” didn’t come up with any result. I was about to give up and make the conclusion that the idea came to life later, when the synopses were written or around so.

But, wait, what interface is mentioned as a topic of a OO-related RFC? Oh, that interface! As the request body states it:

Add a mechanism for declaring class interfaces with a further method for declaring that a class implements said interface.

At this point I realized once again that it is now a full 20 years behind us. That the text is from the times when many considered Java as the only right OO implementation! And indeed, by reading further we find the following statement, likely to be affected by some popular views of the time:

It’s now a compile time error if an interface file tries to do anything other than pre declare methods.

Reminds of something, isn’t it? And then, at the end of the RFC, we find another one:

Java is one language that springs to mind that uses interface polymorphism. Don’t let this put you off – if we must steal something from Java let’s steal something good.

Good? Good?!! Oh, my… Java’s attempt to solve problems of C++ multiple inheritance approach by simply denying it altogether is what drove me away from the language from the very beginning. I was fed up with Pascal controlling my writing style as far back as in early 90s!

Luckily, those involved in early Perl6 design must have shared my view to the problem (besides, Java itself has changed a lot since). So, we have roles now. What they have in common with abstract classes and the modern interfaces is that a role can define an interface to communicate with a class, and provide implementation of some role-specific behavior too. It can also do a little more than only that!

What makes roles different is the way a role is used in Raku OO model. A class doesn’t implement a role; nor it inherits from it as it would with abstract classes. Instead it does the role; or the other word I love to use for this: it consumes a role. Technically it means that roles are mixed into classes. The process can be figuratively described as if the compiler takes all methods and attributes contained by role’s type object and re-plants then onto the class. Something like:

role Foo {
    has $.foo = 42;
    method bar {
        say "hello!"
    }
}
class Bar does Foo { }
my $obj = Bar.new;
say $obj.foo; # 42
$obj.bar;     # hello!

How is it different from inheritance? Let’s change the class Bar a little:

class Baz {
    method bar {
        say "hello from Baz!"
    }
}
class Bar does Foo is Baz {
    method bar {
        say "hello from Bar!";
        nextsame
    }
}
Bar.new.bar; # hello from Bar!
             # hello from Baz!

nextsame in this case re-dispatches a method call to the next method of the same name in the inheritance hierarchy. Simply put, it passes control over to the method Baz::bar, as one can see from the output we’ve received. And Foo::bar? It’s not there. When the compiler mixes the role into Bar it finds that the class does have a method named bar already. Thus the one from Foo is ignored. Since nextsame only considers classes in the inheritance hierarchy, Foo::bar is not invoked.

With another trick the difference from interface consumption can also be made clear:

class Bar {
    method bar {
        say "hello from Bar!"
    }
}
my $obj = Bar.new;
$obj.bar; # hello from Bar!
$obj does Foo;
$obj.bar; # hello!

In this example the role is mixed into an existing object, thanks to the dynamic nature of Raku which makes this possible. When a role is applied this way its content is enforced over the class content, similarly to a virus injecting its genetic material into a cell effectively overriding internal processes. This is why the second call to bar is dispatched to the Foo::bar method and Bar::bar is nowhere to be found on $obj this time.

To have this subject fully covered, let me show you some funny code example. The operator but used in it behaves like does except it doesn’t modify its LHS object; instead but creates and returns a new one:

‌‌my $s1 = "not empty means true";
my $s2 = $s1 but role { method Bool { False } };
say $s1 ?? "true" !! "false";
say $s2 ?? "true" !! "false";

This snippet I’m leaving for you to try on your own because it’s time for my post to move onto another topic: role parameterization.

Consider the example:

role R[Str:D $desc] {
    has Str:D $.description = $desc;
}
class Foo does R["some info"] { }
say Foo.new.description; # some info

Or more practical one:

role R[::T] {
    has T $.val is rw;
}
class ContInt does R[Int] { }
ContInt.new.val = "oops!"; # "Type check failed..." exception is thrown

The latter example utilizes so called type capture where T is a generic type, the concept many of you are likely to know from other languages, which turns into a concrete type only when the role gets consumed and supplied with a parameter, as in class ContInt declaration.

The final iteration for parametrics I’m going to present today would be this more extensive example:

role Vect[::TX] {
    has TX $.x;
    method distance(Vect $v) { ($v.x - $.x).abs }
}
role Vect[::TX, ::TY] {
    has TX $.x;
    has TY $.y;
    method distance(Vect $v) { 
        (($v.x - $.x)² + ($v.y - $.y)²).sqrt 
    }
}

class Foo1  does Vect[Rat]      { }
class Foo2 does Vect[Int, Int] { }

my $foo1 = Foo1.new(:x(10.0));
my $foo2 = Foo2.new(:x(10), :y(5));
say $foo1;                                   # Foo1.new(x => 10.0)
say $foo2;                                   # Foo2.new(x => 10, y => 5)
say $foo2.distance(Foo2.new(:x(11), :y(4))); # 1.4142135623730951

Hopefully, the code explains itself. Most certainly it nicely visualizes the long way made by the language designers since the initial RFC was made.

At the end I’d like to share a few interesting facts about Raku roles and their implementation by Rakudo.

  1. As of Raku v6.e, a role can define own constructor/destructor submethods. They’re not mixed into a class as methods are. Instead, they’re used to build/destroy an object same way, as constructors/destructors of classes do:
use v6.e.PREVIEW; # 6.e is not released yet
role R { submethod TWEAK { say "R" } }
class Foo { submethod TWEAK { say "Foo" } }
class Bar is Foo does R { submethod TWEAK { say "Bar" } }
Bar.new; # Foo
         # R
         # Bar
  1. Role body is a subroutine. Try this example:
role R { say "Role" }
class Foo { say "Foo" }
# Foo

Then modify class Foo so that it consumes R:

class Foo does R { say "Foo" }
# Role
# Foo

The difference in the output is explained by the fact that role body gets invoked when the role itself is mixed into a class. Try adding one more class consuming R alongside with Foo and see how the output changes. To make the distinction between class and role bodies even more clear, make your new class inherit from Foo. Even though is and does look alike they act very much different. 3. Square brackets in role declaration enclose a signature. As a matter of fact, it is the signature of role body subroutine! This makes a few very useful tricks possible:

# Limit role parameters to concrete numeric objects.
role R[Numeric:D ::T $default] {
    has T $.value = $default;
}
class Foo[42.13] { };
say Foo.new.x; # 42.13

Or even:

# Same as above but only allow specific values.
role R[Numeric:D ::T $default where * > 10] {
    has T $.value = $default;
} 

Moreover, in case when few different parametric candidates are declared for a role, choosing the right one is a task of the same kind as choosing the right routine of a few multi candidates and based on matching signatures to the parameters passed. 4. Rakudo implements a role using four different role types! Let me demonstrate one aspect of this with the following snippet based on the example for the previous fact:

for Foo.^roles -> \consumed {
    say R === consumed
}

=== is a strict object identity operator. In our case we can consider it as a strict type equivalence operator which tells us if two types are actually exactly the same one.

And as I hope to have this subject covered later in a more extensive article, at this point I would make it a classical abrupt open ending by providing just the output of the above snippet as a hint:

False

4 thoughts on “RFC 265: Interface polymorphism considered lovely

  1. Very enlightening! Looking forward to “the more extensive article”! But, is there something wrong with the example in 3.? Anyway, didn’nt understand it …

    Like

    1. Consider role Foo from the previous example. When it is consumed in normal way its method Foo::bar is ignored because the class already have such method. But in example 3 the role is mixed in into the class and this results in class method Bar::bar to be overridden by Foo::bar. The point is that despite same does keyword used behaviors in the two cases are different.

      Like

  2. Yeah… i wrote this RFC. I can’t remember when I first saw the traits paper (via chromatic, I think), but it was so obviously the Right Thing, especially when Larry got his hands on it.

    Roles solve the problem I was trying to address and more. I was simply thinking of having usefully typed variable declarations without requiring weird class hierarchies. It was obvious that Roles/Traits were the proverbial better mousetrap.

    Like

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.