Enforcing single instances from the metamodel

Solution 1:

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