Proposed on 25 August 2000, frozen on 16 September 2000
On polymorphism
RFC159 introduces the concept of true polymorphic object.
Objects that can morph into numbers, strings, booleans and much more on-demand. As such, objects can be freely passed around and manipulated without having to care what they contain (or even that they’re objects).
When one looks at how 42
, "foo"
, now
work in Raku nowadays, one can only see that that vision has pretty much been implemented. Because most of the time, one doesn’t really care about the fact that 42
is really an Int
object, "foo"
is really a Str
object and that now
represents a new Instant
object every time it is called. The only thing one cares about, is that they can be used in expressions:
say "foo" ~ "bar"; # foobar
say 42 + 666; # 708
say now - INIT now; # 0.0005243
RFC159 lists a number of method names to be used to indicate how an object should behave under certain circumstances, with a fallback provided by the system if the class of the object does not provide that method. In most cases these methods did not make it into Raku, but some of them did with a different name:
Name in RFC | Name in Raku | When |
---|---|---|
STRING | Str | Called in a string context |
NUMBER | Numeric | Called in a numeric context |
BOOLEAN | Bool | Called in a boolean context |
And some of them even retained their name:
Name in RFC | When |
---|---|
BUILD | Called in object blessing |
STORE | Called in an lvalue = context |
FETCH | Called in an rvalue = context |
DESTROY | Called in object destruction |
but with sometimes subtly different semantics from the RFC.
Only a few made it
In the end, only a limited set of special methods was decided on for Raku. All of the other methods in RFC159 have been implemented by polymorphic operators that coerce when needed. For instance the proposed PLUS
method has been implemented as an infix +
operator that has a “default” candidate that coerces its operands to a number.
So, effectively, if you have an object of class Foo
and you want that to act as a number, one only needs to add a Numeric
method to that class. An expression such as:
my $foo = Foo.new;
say $foo + 42;
is effectively executing:
say infix:<+>( $foo, 42 );
and the infix:<+>
candidate that takes Any
objects, does:
return infix:<+>( $foo.Numeric, 42.Numeric );
And if such a class Foo
does not provide a Numeric
method, then it will throw an exception.
The DESTROY method
In Raku, object destruction is non-deterministic. If an object is no longer in use, it will probably get garbage collected. The probable part is because Raku does not know a global destruction phase, unlike Perl. So when a program is done, it just does an exit
(although that logic does honour any END
blocks).
An object is marked “ready for removal” when it can no longer be “reached”. It then has its DESTROY
method called when the garbage collection logic kicks in. Which can be any amount of time after it became unreachable.
If you need deterministic calling of the DESTROY
method, you can use a LEAVE
phaser. Or if that doesn’t allow you to scratch your itch, you can possibly use the FINALIZER
module.
STORE / FETCH on scalar values
Conceptually, you can think of a container in Raku as an object with STORE
and FETCH
methods. Whenever you set a value in a container, it conceptually calls the STORE
method. And whenever the value inside the container is needed, it conceptually calls the FETCH
method. In pseudo-code:
my $foo = 42; # Scalar.new(:name<$foo>).STORE(42)
But what if you want to control access to a scalar value, similar to Perl’s tie
? Well, in Raku you can, with a special type of container class called Proxy
. An example of its usage:
sub proxier($value? is copy) {
return-rw Proxy.new(
FETCH => method { $value },
STORE => method ($new) {
say "storing";
$value = $new
}
)
}
my $a := proxier(42);
say $a; # 42
$a = 666; # storing
say $a; # 666
Subroutines return their result values de-containerized by default. There are basically two ways of making sure the actual container is returned: using return-rw
(like in this example), or by marking the subroutine with the is rw
trait.
STORE on compound values
Since FETCH
only makes sense on scalar values, there is no support for FETCH
on compound values, such as hashes and arrays, in Raku. I guess one could consider calling FETCH
in such a case to be the Zen slice, but it was decided that that would just return the compound value itself.
The STORE
method on compound values however, allows for some interesting functionality. The STORE
method is called whenever there is an initialization of the entire compound value. For instance:
@a = 1,2,3;
basically executes:
@a := @a.STORE( (1,2,3) );
But what if you don’t have an initialized @a
yet? Then the STORE
method is supposed to actually create a new object and initialize this with the given values. And the STORE
method can tell, because then it also receives a INITIALIZE
named argument with a True
value. So when you write this:
my @b = 1,2,3;
what basically gets executed is:
@b := Array.new.STORE( (1,2,3), :INITIALIZE );
Now, if you realize that:
my @b;
is actually short for:
my @b is Array;
it’s only a small step to realize that you can create your own class with customized array logic, that can replace the standard Array
logic with your own. Observe:
class Foo {
has @!array;
method STORE(@!array) {
say "STORED @!array[]";
self
}
}
my @b is Foo = 1,2,3; # STORED 1 2 3
However, when you actually start using such an array, you are confronted with some weird results:
say @b[0]; # Foo.new
say @b[1]; # Index out of range. Is: 1, should be in 0..0
Without getting into the reasons for these results, it should be clear that to completely mimic an Array
, a lot more is needed. Fortunately, there are ecosystem modules available to help you with that: Array::Agnostic for arrays, and Hash::Agnostic for hashes.
BUILD
The BUILD
method also subtly changed its semantics. In Raku, method BUILD
will be called as an object method and receive all of the parameters given to .new
, after which it is fully responsible for initializing object attributes. This becomes more visible when you use the internal helper module BUILDPLAN
. This module shows the actions that will be performed on an object of a class when built with the default .new
method:
class Bar {
has $.score = 42;
}
use BUILDPLAN Bar;
# class Bar BUILDPLAN:
# 0: nqp::getattr(obj,Foo,'$!score') = :$score if possible
# 1: nqp::getattr(obj,Foo,'$!score') = 42 if not set
This is internals speak for: – assign the value of the optional named argument score
to the $!score
attribute – assign the value 42
to the $!score
attribute if it was not set already
Now, if we add a BUILD
method to the class, the buildplan changes:
class Bar {
has $.score = 42;
method BUILD() { }
}
use BUILDPLAN Bar;
# class Bar BUILDPLAN:
# 0: call obj.BUILD
# 1: nqp::getattr(obj,Foo,'$!score') = 42 if not set
Note that there is no automatic attempt to take the value of the named argument score
anymore. Which means that you need to do a lot of work in your custom BUILD
method if you have many named arguments, and only one of them needs special handling. That’s why the TWEAK
method was added:
class Bar {
has $.score = 42;
method TWEAK() { }
}
use BUILDPLAN Bar;
# class Bar BUILDPLAN:
# 0: nqp::getattr(obj,Foo,'$!score') = :$score if possible
# 1: nqp::getattr(obj,Foo,'$!score') = 42 if not set
# 2: call obj.TWEAK
Note that the TWEAK
method is called after all of the normal checks and initializations. This is in most cases much more useful.
Conclusion
Although the idea of true polymorphic objects has been implemented in Raku, it turned out quite different from originally envisioned. In hindsight, one can see why it was decided to be unpractical to try to support an ever increasing list of special methods for all objects. Instead, a choice was made to only implement a few key methods from the proposal, and for the others the approach of automatic coercions was taken.