Exploring Rebless with Raku

by Arne Sommer

Exploring Rebless with Raku

Published 12. February 2020

See also Part 1: Raku and the (Re)blessed Child for a discussion on breaking changes.

The official documentation has this entry for «rebless»:

(Metamodel::Primitives) method rebless

method rebless(Mu $object, Mu $type)

Changes $object to be of type $type. This only works if $type type-checks against the current type of $object, and if the storage of $object is a subset of that of $type.

«subset» is a Raku keyword used to set up custom types (as in e.g. «subset Positive of Int where * > -1;» which defines a type that allows positive integers (including 0) only), but that doesn't make sense her. So surely it means inheritance and a subclass.

Here is a very short introduction to classes and subclasses:

File: person-mro
class Person          { ; }      # [1]
class Woman is Person { ; }      # [2]

my $tom   = Person.new;          # [3]
my $lisa  = Woman.new;           # [4]

say "Tom belongs to the class { $tom.^name }";              # [5]
say "Tom is { 'not ' unless $tom.isa(Person)   }a Person";  # [6]
say "Tom is { 'not ' unless $tom.isa(Woman)    }a Woman";   # [6a]

say "Lisa belongs to the class { $lisa.^name }";            # [7]
say "Lisa is { 'not ' unless $lisa.isa(Person) }a Person";  # [8]
say "Lisa is { 'not ' unless $lisa.isa(Woman)  }a Woman";   # [8a]

say "Person MRO:", Person.^mro;  # [9]
say "Woman  MRO:", Woman.^mro;   # [10]

Running it:

$ person-mro
Tom belongs to the class Person           # [5]
Tom is a Person                           # [6]
Tom is not a Woman                        # [6a]
Lisa belongs to the class Woman           # [7]
Lisa is a Person                          # [8]
Lisa is a Woman                           # [8a]
Person MRO:((Person) (Any) (Mu))          # [9]
Woman  MRO:((Woman) (Person) (Any) (Mu))  # [10]

[1] A «Person» class, with no content (the { ; } part).

[2] A «Woman» class (or subclass) that inherits from «Person» (with the «is» keyword). Also empty.

[3] An object of the «Person» class.

[4] An object of the «Woman» class.

[5] The «^name» method gives us the class name for the «$tom» object, which is «Person».

[6] The «$tom» object belongs to the «Person» class, and not to the «Woman» class [6a] (obviously).

[7] As [5], but «$lisa» and «Woman».

[8] The «$lisa» object belongs to the «Person» class, as well as the «Woman» class [8a]. This is a result of the inhertance.

[9] The inherticance tree for the «Person» class. The «Any» and «Mu» classes are built-in, but you can ignore them. «mro» stands for «method resolution order». The search starts from the left.

[10] Note that «Woman» has been added before «Person», so a «Woman» is also a «Person». (And I think that I'll have to find another example, before becoming politically incorrect.)

Rebless, First Try

Let us have a go at it:

File: rebless-error
class Person          { ; }
class Woman is Person { ; }

my $tom   = Person.new;
my $lisa  = Woman.new;

say $tom.^name;   # -> Person
say $lisa.^name;  # -> Woman

Metamodel::Primitives.rebless($tom, Woman);

say $tom.^name;   # -> Woman

This subclass approach worked until march 2019, but not any more:

$ raku rebless-error
Person
Woman
New type Woman for Person is not a mixin type

The error message mentions «mixin type», which is something we get with a «role» that we add (or mix in) to an object.

We want a class name (or rather, something that looks and behaves like a class name), and a little trickery is required to make that work:

File: rebless
class    Person                  { ; }      # [1]
constant Woman = Person but role { ; }      # [2]

Woman.^set_name('Woman');                   # [3]

my $tom   = Person.new;
my $lisa  = Woman.new;

say $tom.^name;   # -> Person 
say $lisa.^name;  # -> Woman

Metamodel::Primitives.rebless($tom, Woman); # [4]

say $tom.^name;   # -> Woman 

[1] As before.

[2] Instead of a subclass, we mix in a role (with «but»). «constant» gives us a read-only variable.

[3] This one ensures that the new type reports its name as «Woman». If we skip this line, we get something like «Person+{<anon|1>}» instead.

[4] This works now, as we have used a mixin, and not a subclass.

Running ut:

$ raku rebless
Person
Woman
Woman

But what if we have a circular reference, so that we refer to something that hasn't been defined yet?

We can do it with procedures, without problems:

File: forward-definition
do-something;

sub do-something
{
  say "Whatever...";
}

Running it:

$ raku forward-definition
Whatever...

But it does not work with variables or classes:

say $a;
my $a = 12;
    
class Child  { has Parent $.parent; }
class Parent { has Child  $.child;  }

The solution to the class problem is a stubbed class, that we redefine before it is used:

File: stubbed-class
class Parent { ... }

class Child { has Parent $.parent; }

class Parent { has Child $.child; }

That does not work for mixins, but if we store the class-like definition (the mixin) in a variable (instead of declaring it «constant» as we did in the «rebless» program), we can define it up front, and change the value afterwards.

The follwing example has two classes «Child» and «Adult», and the «Child» object changes to an «Adult» object when it reaches 18 years of age.

File: vote
my $Adult;                   # [1]

class Child                  # [2]
{
  has Int $.age is rw = 0;   # [3]

  method happy-birthday      # [4]
  {
    $.age++;                 # [4a]
    Metamodel::Primitives.rebless(self, $Adult) if $.age == 18; # [4b]
  }
  
  method can-vote            # [5]
  {
    False;
  }
}

$Adult = Child but role { method can-vote { True } } # [6]

$Adult.^set_name('Adult');  # [7]

my $tom = Child.new;        # [8]

say "Age  Can-Vote  Class"; 

for ^20                     # [9]
{
  say "{ $tom.age.fmt('%3d') }   { $tom.can-vote }    { $tom.^name }";
  $tom.happy-birthday;
}

[1] We store the classlike thing in this variable. For now, we declare it so that [4b] doesn't fail

[2] The Child class,

[3] with the age (in years). The field is set up as read write («rw»), so is not protected in any way. That is a security issue, but that is outside of the scope of this article.

[4] with a method «happy-birtday» that increases the age with one year. When the age reaches 18, the class is changed to «$Adult» [4b]

[5] with a method «can-vote» that returns «False».

[6] Then we set up the classlike thing. Note the role content that overrides the «can-vote» method.

[7] Fix the name of the classlike thing.

[8] Start the show.

[9] Run a loop 20 times, and see what happens when the child reaches 18 years of age.

Running it:

Age  Can-Vote  Class
  0   False    Child
  1   False    Child
  2   False    Child
  3   False    Child
  4   False    Child
  5   False    Child
  6   False    Child
  7   False    Child
  8   False    Child
  9   False    Child
 10   False    Child
 11   False    Child
 12   False    Child
 13   False    Child
 14   False    Child
 15   False    Child
 16   False    Child
 17   False    Child
 18   True    Adult
 19   True    Adult

Last Words

The «rebless» examples are just meant to illustrate the point. Both programs would be better off with another approach.

The fact that «rebless» is not available on the object itself (e.g. «$object.rebless(New-Class)», but through the convoluted «Metamodel::Primitives.rebless($object, New-Class)» syntax should give a hint that this is something that probably isn't meant for everyday usage.

See also Part 1: Raku and the (Re)blessed Child for a discussion on breaking changes.