Extra-Typical Perl 6
Have you ever grabbed an Int
and thought, "Boy! I sure would enjoy having an .even method on it!" Before you beg the core developers on IRC to add it to Perl 6, let's review some user-space recourse available to you.
My Grandpa Left Me a Fortune
One way to go is to define your own custom Int-like class that knows how to perform the .even
method. You don't have to reinvent the wheel, just inherit from the Int
type. You can mix and match core Perl 6 types and roles any way that pleases you.
class BetterInt is Int {
method even { self %% 2 }
}
my BetterInt $x .= new: 42;
say $x.even;
$x .= new: 71;
say $x.even;
say $x + 42;
# OUTPUT:
# True
# False
# 113
We created a BetterInt
class and inherited from Int
using is Int
trait. The class body has just
the extra method even
we want to add. Using such a class requires a bit of extra code, however.
The my BetterInt $x
part restricts $x
to contain objects of just BetterInt
or subclasses. The
.= new: 42
in this case is the same as = BetterInt.new: 42
(it's a shorthand method-call-assign notation, same as +=
is a shorthand to add to original value).
If we ever want to change the value, we have to do the same .= new:
trick again to get a BetterInt
inside of our container or else we'll get a fatal error.
The good news, however, is that math operators work just fine on our new
class, and it's even accepted by anything that wants to have an Int
. Here's a sub that expects an
Int
but happily gobbles up our BetterInt
:
sub foo (Int $x) { say "\$x is $x" }
my BetterInt $x .= new: 42;
foo $x;
# OUTPUT:
# $x is 42
But... But... But...
Another option is to mix in a role.
The but
infix operator creates a copy of an object and does just that:
my $x = 42 but role { method even { self %% 2 } };
say $x.even;
# OUTPUT:
# True
The role doesn't have to be inlined, of course. Here's another example that uses a pre-defined role and also shows that our object is indeed a copy:
role Better {
method better { 'Yes, I am better' }
}
class Foo {
has $.attr is rw
}
my $original = Foo.new: :attr<original>;
my $copy = $original but Better;
$copy.attr = 'copy';
say $original.attr; # still 'original'
say $copy.attr; # this one is 'copy'
say $copy.better;
say $original.better; # fatal error: can't find method
# OUTPUT:
# original
# copy
# Yes, I am better
# Method 'better' not found for invocant of class 'Foo'
# in block <unit> at test.p6 line 18
This is great and all, but as far as our original goal is concerned, this solution is rather weak:
my $x = 42 but role { method even { self %% 2 } };
say $x.even; # True
$x = 72;
say $x.even; # No such method
The role is mixed into our object stored inside the container, so as soon as we put a new value into the container, our fancy-pants .even
method is gone, unless we mix in the role again.
Sub it in
Did you know you can call subs as methods? It's pretty neat! You receive the object as the first positional parameter and you can even continue the method chain, with a caveat that you can't break up those chains onto multiple lines if the &sub method call doesn't remain on the first line:
sub even { $^a %% 2 };
say 42.&even.uc;
# OUTPUT:
# TRUE
This does serve as a decent way to add extra functionality to core types. The $^a
inside our sub's
definition refers to the first parameter (the object we're making the call on)
and the entire sub can be written differently as sub ($x) { $x %% 2 }
. And,
of course, your sub-now-method can take arguments too.
Here Be Dragons
The docs for what I'm about to describe contain words "don't do this" at the beggining. No matter what the JavaScript folks might tell you, augmenting native types is dangerous, because you're affecting all parts of your program—even modules that don't see your augmentation.
Now that I have the right to tell you 'I told you so' when the nuclear plant you work at melts down, let's see some code:
# Foo.pm6
unit module Foo;
sub fob is export {
say 42.even;
}
# Bar.pm6
unit module Bar;
use MONKEY-TYPING;
augment class Int {
method even { self %% 2 }
}
# test.p6
use Foo;
use Bar;
say 72.even;
fob;
# OUTPUT:
# True
# True
All of the action is happening inside Bar.pm6
. First, we have to write a use MONKEY-*
declaration, which is there to tell us we're doing something dangerous. Next, we use keyword augment
before class Int
to indicate we want to augment the existing class. Our augmentation adds method even
that tells whether
the Int is an even number.
Now, let's look at the whole program. We have module Foo
that gives us one sub that simply prints the result of a call of .even
on 42
(which is an Int
). We use
Foo
BEFORE we use Bar
, the module with our augmentation. Lastly, in our script, we call method .even
on an Int
and then make a call to the sub exported by Foo
.
The scary thing? It all works! Both 72
in our main script and 42
inside the sub in Foo
now have method .even
, all thanks to our augmentation we performed inside Bar.pm6
. We got what we wanted originally, but it's a dangerous method to use.
Evil Flows Through Me
If you're still reading this, that means you're not above messing everything up, core or not. We augmented an Int
type, but our numbers can exist as types other than that. Let's augment the Cool
type to cover all of 'em:
use MONKEY-TYPING;
augment class Cool {
method even { self %% 2 }
}
.say for 72.even, '72'.even, pi.even, ½.even;
# OUTPUT:
# Method 'even' not found for invocant of class 'Int'
# in block <unit> at test.p6 line 8
Oops. That didn't work, did it? As soon as we hit our first attempt to call .even
(on Int
72), the
program crashed. The reason for that is all the types that derive from Cool
were already composed by the
time we augmented Cool
. So, to make it work we have to re-compose them with .^compose
Meta Object Protocol method:
use MONKEY-TYPING;
augment class Cool {
method even { self %% 2 }
}
.^compose for Int, Num, Rat, Str, IntStr, NumStr, RatStr;
.say for 72.even, '72'.even, pi.even, ½.even;
# OUTPUT:
# True
# True
# False
# False
It worked! Now Int, Num, Rat, Str, IntStr, NumStr, RatStr
types have an .even
method
(note: those aren't the only types that inherit Cool
)! This is both equisitely evil and plesantly awesome.
Conclusion
When extending functionality of Perl 6's core types or any other class, you have several options. You can use a subclass with is Class
. You can mix in a role with but Role
. You can call subroutines as
methods with $object.&sub
. Or you can come to the dark side and use augmentation.
Perl 6—There Is More Than One Way To Extend it.
Leave a comment