I have a class that java would call a "bean". I would like to be able to merge in values for attributes in stages but also check that anything merged matches the submethod BUILD signature:
class A {
has Int $.a;
has Str $.b;
submethod BUILD ( Int :$!a, Str :$!b ) { }
my Signature $sig = ( A.^methods.grep: { .name eq 'BUILD' } )[0].signature;
method fill-in ( *%args ) {
say 'filling-in';
my @args = %args.pairs;
@args.unshift: self;
say 'after unshift';
self.raku.say;
my Capture $arg-capture = @args.Capture;
say 'after Capture';
self.raku.say;
unless $arg-capture ~~ $sig {
warn '(warning)';
return;
}
my Str $name;
my %merged;
say 'after check';
self.raku.say;
for self.^attributes -> Attribute $a {
$name = $a.name.substr: 2, *;
%merged{ $name } = ( $a.get_value: self ) // %args{ $name } // $a.type;
}
self.BUILD: |%merged.Capture;
}
}
my A $a = A.new: b => 'init';
$a.fill-in: a => 1;
outputs
filling in
after unshift
A.new(a => Int, b => "init")
after Capture
A.new(a => Int, b => "init")
after check
A.new(a => 1, b => Str)
if the @args.unshift: self is changed to @args.unshift: A, then after Capture it dies with
Cannot look up attributes in a A type object...
I realize I don't need to do this since the fill-in code only considers attributes that exist in the class but wonder if clearing the values from the Capture invocant when checking whether the signature will accept it is the expected behavior?
unshifting a throw-away instance ( @args.unshift: A.new ) works around the behavior.
Signature binding in Raku works left to right through the arguments, calculating a default value if needed, performing type checks, and binding the value. It then proceeds to the next argument. This is required in order that where clauses and later defaults refer to earlier arguments, for example in:
sub total-sales(Date $from, Date $to = $from) { ... }
In the case of attributive binding, that also takes place immediately after processing of the parameter, since in one might write something like:
submethod BUILD ( Int :$!a, Str :$!b = ~$!a ) { }
When one smartmatches a signature, it works by creating an invocation of the code object that owns the signature, in order that there is a place to resolve and look up the parameter values (for example, in the Date $to = $from, it needs to store and later resolve the $from). That invocation record is then discarded afterwards, and you need never think about it.
Attributive parameters will be bound into the object that is passed as the invocant - that is, the first argument. In the case of the parameter :$!b, the behavior is the same as when there's no b named argument: the default value is used, which in this case is the type object Str. Thus the clearing of the attributes in the object as a side-effect of the binding is expected.
A further upshot of this is that since binding is not transactional, then in something like:
method m(Int $!a, Int $!b) { }
A call $obj.m(1, "42") would update $!a, then throw an exception. Probably this is not the semantics you want. Perhaps better, since you're doing the .^attributes dance anyway here, is to instead do it all that way:
class A {
has Int $.a;
has Str $.b;
method fill-in(*%args --> Nil) {
my @attrs;
my @values;
for self.^attributes -> Attribute $attr {
my $shortname = $attr.name.substr(2);
if %args{$shortname}:exists {
my $value = %args{$shortname};
unless $value ~~ $attr.type {
warn '(warning)';
return;
}
@attrs.push($attr);
@values.push($value);
}
}
@attrs.map(*.get_value(self)) Z= @values;
}
}
my A $a = A.new: b => 'init';
$a.fill-in: a => 1;
note $a;
Noting that this also eliminates the need to write a boilerplate BUILD and, better still, the method fill-in can now be extracted to a role and used in All The Beans.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With