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?
There's a few misunderstandings in this attempt.
my
. A my
would mean there's one global object no matter which type we create.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.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