Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Raku rebless and multiple classes

Tags:

raku

(This is a follow up to: Raku rebless doesn't work with inherited classes anymore)

I have tried to come up with a more complex use case, but am unable to get the code to work.

The idea is a Person class, with mixin subclasses for Child and Adult. We have a Child object, and change the type to Adult when the age passes 18 year.

This one obviously fails, as Adult is a mixin on Parent, and not on Child:

class Person
{
  has Int $.age is rw = 0;

  method happy-birthday
  {
    $.age++;
    # Metamodel::Primitives.rebless($, Adult) if $.age == 18;
  }

  method can-vote
  {
    ...;
  }
}

constant Adult = Person but role { method can-vote { True  } }

constant Child = Person but role
{
  method can-vote { False }
  method happy-birthday
  {
    $.age++;
    Metamodel::Primitives.rebless(self, Adult) if $.age == 18;
  }

}

BEGIN Child.^set_name('Child');
BEGIN Adult.^set_name('Adult');

my $tom   = Child.new;

say "Age  Can-Vote  Class";

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

But it runs partially:

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
Incompatible MROs in P6opaque rebless for types Child and Adult
  in method happy-birthday at ./vote-error line 28

Setting it up with just one class and one mixin is the thing:

class Child
{
  has Int $.age is rw = 0;

  method happy-birthday
  {
    $.age++;
    Metamodel::Primitives.rebless($, Adult) if $.age == 18;
  }

  method can-vote
  {
    False;
  }
}

constant Adult = Child but role { method can-vote { True } }

BEGIN Adult.^set_name('Adult');

my $tom = Child.new;

say "Age  Can-Vote  Class";

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

Except that it doesn't work:

 Error while compiling vote-error1
Illegally post-declared type:
    Adult used at line 10

I get that. The rebless line refers to Adult, which hasn't been declared yet. So I tried stubbing the class:

class Child { ... }

constant Adult = Child but role { method can-vote { True } }

class Child
{
  has Int $.age is rw = 0;

  method happy-birthday
  {
    $.age++;
    Metamodel::Primitives.rebless($, Adult) if $.age == 18;
  }

  method can-vote
  {
    False;
  }
}

BEGIN Adult.^set_name('Adult');

my $tom = Child.new;

say "Age  Can-Vote  Class";

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

But stubbing and inheritance doesn't like each other:

===SORRY!=== Error while compiling vote-error2
'Child+{<anon|1>}' cannot inherit from 'Child' because 'Child' isn't composed yet (maybe it is stubbed)

Then I tried adding a new mixin to avoid the circular reference problem:

class Child
{
  has Int $.age is rw = 0;

  method can-vote
  {
    False;
  }
}

constant Adult = Child but role { method can-vote { True } }
BEGIN Adult.^set_name('Adult');

role still-a-child
{
  method happy-birthday
  {
    $.age++;
    Metamodel::Primitives.rebless($, Adult) if $.age == 18;
  }
}

my $tom = Child.new but still-a-child;

say "Age  Can-Vote  Class";

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

But that failed as well:

Age  Can-Vote  Class
  0   False    Child+{still-a-child}
  1   False    Child+{still-a-child}
  2   False    Child+{still-a-child}
  3   False    Child+{still-a-child}
  4   False    Child+{still-a-child}
  5   False    Child+{still-a-child}
  6   False    Child+{still-a-child}
  7   False    Child+{still-a-child}
  8   False    Child+{still-a-child}
  9   False    Child+{still-a-child}
 10   False    Child+{still-a-child}
 11   False    Child+{still-a-child}
 12   False    Child+{still-a-child}
 13   False    Child+{still-a-child}
 14   False    Child+{still-a-child}
 15   False    Child+{still-a-child}
 16   False    Child+{still-a-child}
 17   False    Child+{still-a-child}
Cannot change the type of a Any type object
  in method happy-birthday at vote-error3 line 26

And it did as $tom is now something else than a Child, and Adult isn't a mixin of what we now have. But the error message isn't very helpful.

The last one is essentially the same as the first one.

And I am stuck.

like image 792
Arne Sommer Avatar asked Feb 04 '20 05:02

Arne Sommer


Video Answer


2 Answers

TL;DR I describe several issues. I show a solution at the end that compiles and runs on a recent (2020) Rakudo. It's a simple variant of your own code but I am not knowledgeable enough to vouch for its correctness let alone appropriateness[1] [2].

Cannot change the type of a Any type object

The error message comes from the rebless line:

Metamodel::Primitives.rebless($, Adult) if $.age == 18;

A $ as a term[3] does not mean self but instead an anonymous state Scalar variable. By default it contains an Any, hence the error message. It should be self.[4]

Having fixed this first problem, we get a new one depending on which Rakudo version is used:

  • Older Rakudo: Incompatible MROs in P6opaque rebless for types Child and Adult.

  • Newer Rakudo: New type Adult for Child is not a mixin type.

Like the first error message we just fixed, these two are also triggered by the rebless statement.[5]

We must solve both problems.

In a newer Rakudo, fixing the Cannot change the type of a Any type object and the not a mixin type problems aren't enough if I use your "adding a new mixin" code; I just get the Incompatible MROs error.

Conversely, using alternate code that fixes the Incompatible MROs problem on an older Rakudo leads to the not a mixin type unless that problem is properly addressed. (In my original version of this answer I solved the Incompatible MROs problem -- and then neglected to test on a newer Rakudo!)

Your diagnosis of the Incompatible MROs error was "This one obviously fails, as Adult is a mixin on Person, and not on Child". I read that, glanced at the code, believed you, and moved on. But then I'd arrived back at the same problem using code you'd written to try address it. What gives?

Based on my experiments, it seems that not only must the "to" class (whose class is to be the new class of the object being reblessed) have an MRO that's compatible with the object being reblessed according to things I would expect (like class inheritance) but also the "from" object (the one being reblessed) cannot be both:

  • Based on a class that has attributes.

  • Already mixed into.

(I don't know if this is a bug that can be fixed or an unavoidable restriction. I do know a recent (2020) Rakudo has this constraint using both variations of the code Jonathan provided in the previous SO.)

This means that "adding a new mixin to avoid the circular reference problem" ("stubbing and inheritance doesn't like each other") doesn't solve your problem.

Instead, I went back to your "just one class and one mixin" attempt (which ended up with Illegally post-declared type in the form you originally wrote it) and tried another approach to get around that error.

The following variant of your "just one class and one mixin" code works on a Rakudo v2020.01.114.gcfe.2.cdc.56. All I've done is turned the Adult constant into a variable. I've written ... for the rest of the code which is the same as your code:

my $Adult;

...
    Metamodel::Primitives.rebless(self, $Adult) if $.age == 18;
...

$Adult = Child but role { method can-vote { True } }
$Adult.^set_name('Adult');

...

Hth.

Footnotes

[1] Jonathan's solution in a recent SO used compile-time constructs for Adult. My solution follows Jonathan's example except that it constructs the rebless target $Adult at run-time. I'm unsure if this is technically safe in the face of the new optimization @JonathanWorthington has introduced. I will try to "summon" him to comment on it.

[2] Other than this footnote, my answer does not address the wisdom of using rebless. Two issues come immediately to mind for me. First is reliable functionality given turophilia, which is clearly central to you even needing to ask your recent SOs. (And with it, metaturophilia. That is, we currently have holes in our approach to maturing Raku, the language, and Rakudo, the implementation. To the degree code written by one of us leads to holes being filled in we can all be grateful.) Second is reliable documentation of the MOP given that (as far as I can tell) some key documentation breaks the general rule of constraining itself to the Raku specification according to roast and instead "largely reflects the metaobject system as implemented by the Rakudo Raku compiler". I just address errors until your code compiles and runs without error on a 2020 version of Rakudo.

[3] See What is a term? as linked to with some context in this comment.

[4] Some folk might presume that if $.foo is a .foo of self, then $ must be self. Such thinking would be a reasonable presumption if raku had the typical context-free tokenization used for most programming languages. Moreover, it generally applies to Raku code too, just as it generally applies even in natural language. (If the English token "my" is followed by "self", then it likely means the same as "myself".) But Raku's grammar deliberately combines context sensitivity, scannerless parsing and maximal munch in order to support creation of more natural feeling languages than is typical for programming languages. And here we see an example. In "term position"[3] the input $.foo is recognized as a single token instead of two ($ followed by .foo) whereas the input $,... is recognized as two tokens ($ followed by the list separator operator ,) rather than one.

[5] All of these error messages are generated in parts of Rakudo that are close to the metal. If you're using MoarVM as your backend they come from its P6opaque.c file.

like image 59
raiph Avatar answered Sep 24 '22 08:09

raiph


Thank you. But I am unable to make it work:

class Adult { ... }

class Child
{
  has Int $.age is rw = 0;

  method happy-birthday
  {
    $.age++;
    Metamodel::Primitives.rebless(self, Adult) if $.age == 18;
  }

  method can-vote
  {
    False;
  }
}

role grown-up { method can-vote { True } }

class Adult is Child does grown-up { }

my $tom = Child.new;

say "Age  Can-Vote  Class";

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

This runs to 17, then fails with

New type Adult for Child is not a mixin type

What am I missing?

(And Stackowerflow should allow code in comments.)

like image 28
Arne Sommer Avatar answered Sep 22 '22 08:09

Arne Sommer