Raku rebless doesn't work with inherited classes anymore

ぃ、小莉子 提交于 2020-02-03 04:34:06

问题


The code given in this thread doesn't work anymore: How can I rebless an object in Perl 6?

I wrote this piece of code last year, and it worked then. Now it doesn't:

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);
# -> New type Woman for Person is not a mixin type

The error message doesn't make sense, as it is supposed to work with inherited classes. At least it was.

The documentation is not helpful; https://docs.raku.org/routine/rebless


回答1:


it is supposed to work with inherited classes

It never was supposed to be that general. I designed that API and implemented it in the first place, and it was only ever intended as an implementation detail of mixins.

Until very recently, it was not part of the language specification test suite - and when it did become part of it, it already had its current, more restrictive, semantics. The constraints on it are important for performance reasons: when we know a type is not one that can be the target of a mixin operation, we can JIT-compile attribute accesses on that object into something much simpler (we paid an extra conditional move on every attribute access before the change, and now only have to pay it on mixin target types).

It's possible to modify the original program to work by using the MOP to construct the class. In fact, the following isn't quite the original program; I did a small tweak for the sake of showing how one can provide methods in the subclass as an anonymous role, so as to avoid too much MOP boilerplate.

class Person { method m() { "person" } }
constant Woman = do {
    my \w = Metamodel::ClassHOW.new_type(:is_mixin, :name<Woman>);
    w.^add_parent(Person);
    w.^add_role(role { method m() { "woman" } });
    w.^compose()
}
my $tom = Person.new;
my $lisa = Woman.new;

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

say $tom.m; # person
Metamodel::Primitives.rebless($tom, Woman);
say $tom.m; # woman

While that's the most semantically direct fix to the original program, there is a shorter way: use the but operator on the Person type object to produce a mixin type and return it, and then just tweak its name to your liking:

class Person { method m() { "person" } }
constant Woman = Person but role { method m() { "woman" } }
BEGIN Woman.^set_name('Woman');

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

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

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

Which is only one line extra than the original anyway.




回答2:


See jnthn's authoritative answer.


I've revised and undeleted this answer for posterity, mostly in case someone's interested in another take on "ouch, it hurts when things break". In particular, I've written footnotes that expand on this topic.

Arne: The code given in this thread doesn't work anymore: How can I rebless an object in Raku?

I've updated the accepted answer to that SO to link to this SO.

Arne: I wrote this piece of code last year, and it worked then. Now it doesn't

The relevant change was discussed in an April 2019 commit in which jnthn wrote:

Recently, types that were the target of a rebless operation started needing to be created explicitly as mixin target types, to assist optimization. ...

In a comment 11 days ago closing rakudo GH issue "Rebless to a custom type no longer seems to work", he wrote:

You'll need to arrange to have the is_mixin named argument passed to ClassHOW.new_type ... There's no way to do that with the class syntax, thus the target type of the rebless shall have to be assembled using the MOP also.

(Click above link for notes on how to do what it suggests.)

Arne: it is supposed to work with inherited classes. At least it was.

roast -- the repository of all spec tests -- determines what Raku code is supposed to do. (The st of roast can be read as supposed tos.)

In another April 2019 message jnthn wrote:

There was no previous spec for Metamodel::Primitives.rebless. I've added this spectest so that now there is. This means there's now some definition of what can be expected to work.

The fact that Rakudo's behavior is spec'd by an executable test suite is a fundamental part of @Larry's concept for ensuring Raku behaves reliably[1] and has profound implications[2].

Arne: The documentation is not helpful; https://docs.raku.org/routine/rebless

JJ has updated it.

Though, for the record, the change happened around the time of the Rakudo 2019.03.1 compiler version.[3]

The impact of this change on a widely used module

I'll close this answer with a snapshot of the impact of this change unfolding for Inline::Perl5.

In April 2019, niner opened a rakudo GH issue on the impact on Inline::Perl5 and I've extracted some highlights of the exchange between niner and jnthn below.

(I've elided some stuff that was important in the original context, but distracting in the context of this SO. Please don't assume you have a complete understanding of the original conversation from this extract. If in doubt, click the link.)

niner: TBH what I do here has probably always been kinda fishy ... Could even be that ... I can get rid of [it] ... Would be nice though to keep already deployed Inline::Perl5 versions up and running.

jnthn: There was no previous spec for Metamodel::Primitives.rebless. I've added [a] spectest so that now there is. This means there's now some definition of what can be expected to work, and which Inline::Perl5 can rely on.

Since unknown named parameters are ignored, but :mixin was not required on previous Rakudo versions, then it would be possible to make a new Inline::Perl5 release that can work on previous Rakudo versions as well as the upcoming one, so there can at least be back-compat.

I don't think there's any way of keeping things working for existing Inline::Perl5 versions ...

niner: Unfortunately passing :mixin doesn't help in this case as the rebless is done on a subclass of the one created via Metamodel::Primitives.create_type. The subclass uses the normal Perl6::ClassHOW.

I'm working on a major refactor to get rid of the rebless hack in the first place. I'm reopening this issue so the release manager is aware of there being no working Inline::Perl5 on rakudo's release candidate.

jnthn: Do you create that class using the MOP? You can pass :is_mixin to Perl6::ClassHOW.new_type if so.

niner: No, it's for this situation: class Bar is Foo { }

Footnotes

[1] Said a few minutes after Larry had first announced the project that led to Raku in his 2000 "State of the Onion" speech:

Question: Will [Raku] have specs?

Larry: what we particularly want to stress ... is not perhaps so much the [language design] spec as developing our current regression test ... into a validation test of what the language actually means and actually go out and explore all the nooks and crannies and say, “This is [Raku], this is not [Raku],” and then we actually have a machine-readable spec. And to me that’s actually a lot more important than what the verbiage in the human readable thing says.

[2] Of course, roast only works for a given user if its tests sufficiently cover the user's needs. Arne's problem demonstrates how holes in coverage can be surprising. For discussion of these holes as they stood in 2018, see On Specs, Versioning, Changes, and… Breakage. The good news is that roast is just lots of unit tests written in Raku to test that expressions or constructs with particular values do a particular thing. So it's easy for individuals or corporations to contribute new tests to improve test coverage. And it's all under version control (git), so custom downstream tags, branches and forks are viable, sustainable, and manageable. (Indeed, that's how new language versions (Christmas, Diwali, Eid(?), etc.) are managed.)

[3] I've seen an attempt to rebless a new class created using regular newclass is oldclass syntax both work (on my laptop) and not work (on repl.it) using compilers that claim to be 2019.03.1. (Presumbly repl.it installed a version of the compiler source code, or a binary compiled from it, taken from the master head shortly after the compiler's version was updated to 2019.03.1, with the breaking change in place. I note that repl.it haven't publicized their online raku repl -- I discovered it by accident -- so there's nothing untoward about this situation but it reinforced for me the need for the $RAKU.compiler.verbose-config method used in the worked/broken outputs I just linked.)




回答3:


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.

And I am stuck.



来源:https://stackoverflow.com/questions/59845201/raku-rebless-doesnt-work-with-inherited-classes-anymore

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!