Enforcing single instances from the metamodel
Asked Answered
G

1

5

I know that it's way easier to ensure single instances from the class level, and that there's the excellent Staticish module from Jonathan Stowe that does the same by using roles, but I just want to try and understand a bit better how the class higher order working can be handled, mainly for a FOSDEM talk. I could think of several ways of doing to at the metamodel level, but eventually this is what I came up with:

my class MetamodelX::Singleton is Metamodel::ClassHOW {

    my \instance = Mu;

    method compose(Mu \type) {
        my &callsame := CORE::<&callsame>; # Workaround for RT #127858
        self.method_table(type)<new>.wrap: -> \SELF, | {
            unless instance.defined { instance = SELF };
            callsame();
        };
    }
}

my package EXPORTHOW {
    package DECLARE {
        constant singleton = MetamodelX::Singleton;
    }
}

Mainly ripped from the OO::Monitors code, written as far as I understand it, by JJ Atria and Jonathan Worthington)

Main rationale is trying to wrap the building submethod, or somehow whatever tries to create a new instance of the object. This, however (as well as the same with BUILD and BUILDALL, close to the original), fails with:

No such method 'wrap' for invocant of type 'Any'.  Did you mean any of
these: 'Map', 'WHAT', 'grep', 'map'?

So quite clearly I'm not getting what these do, or for that matter the whole HOW concept. So any idea what might be failing here, or any other way to override the building of the object at the metamodel level to be able to do as intended?

Garbers answered 16/1, 2022 at 11:43 Comment(0)
W
6

There's a few misunderstandings in this attempt.

  1. There is one instance of a meta-class per type. Thus if we want to allow a given type to only be instantiated once, the correct scoping is an attribute in the meta-class, not a my. A my would mean there's one global object no matter which type we create.
  2. The compose method, when subclassing ClassHOW, should always call back up to the base compose method (which can be done using callsame). Otherwise, the class will not be composed.
  3. The method_table method returns the table of methods for this exact type. However, most classes won't have a new method. Rather, they will inherit the default one. If we wrap that, however, we'd be having a very global effect.

While new is relatively common to override to change the interface to construction, the bless method - which new calls after doing any mapping work - is not something we'd expect language users to be overriding. So one way we could proceed is to just try installing a bless method that does the required logic. (We could also work with new, but really we'd need to check if there was one in this class, wrap it if so, and add a copy of the default one that we then wrap if not, which is a bit more effort.)

Here's a solution that works:

my class MetamodelX::Singleton is Metamodel::ClassHOW {
    has $!instance;

    method compose(Mu \type) {
        self.add_method(type, 'bless', -> \SELF, |c {
            without $!instance {
                $!instance := SELF.Mu::bless(|c);
            }
            $!instance
        });
        callsame();
    }
}

my package EXPORTHOW {
    package DECLARE {
        constant singleton = MetamodelX::Singleton;
    }
}

Note that we can't use callsame inside of the code we add for bless because it's not actually a method. We could instead write it to use an anon method, but then we have the problem that the method has its own idea of self, and so we end up having to save the meta-class self and arrange some other means to access $!instance.

Finally, an example of it in action:

use Singleton;

singleton Counter {
    has $.x;
    method inc() { $!x++ }
}

my $c1 = Counter.new;
$c1.inc;

my $c2 = Counter.new; # Gets same instance as in $c1
$c1.inc;
$c2.inc;

say $c1.x; # 3 
say $c2.x; # 3
Walkling answered 16/1, 2022 at 16:2 Comment(2)
If I got this correctly, what you're doing then by creating a class-specific bless is the generation of one for the class when the rest of the class is composed. When the BUILDPLAN is created in github.com/rakudo/rakudo/blob/master/src/Perl6/Metamodel/…, it will not need to add that one, is that correct?Garbers
@Garbers No, there is never a generated bless, everything inherits the one in Mu - unless it defines one, which I've pretty much never seen. You're maybe thinking of BUILDALL, which is generated.Walkling

© 2022 - 2024 — McMap. All rights reserved.