Using the default constructor
The default .new
constructor maps named arguments to public attributes.
In your example, you pass a hash as a positional argument. You can use the |
syntax to interpolate the hash entries into the argument list as named arguments:
$x = Local::Class.new(|%hash);
However, note that this will cause problems if your class has an array attribute like has @.z
:
class Local::Class {
has $.x;
has $.y;
has @.z;
}
my %hash = x => 5, y => 20, z => [1, 2];
my $x = Local::Class.new(|%hash);
say $x; # Local::Class.new(x => 5, y => 20, z => [[1, 2],])
This is because like all hashes, %hash
places each of its values in an item container. So the attribute will be initialized as @.z = $([1, 2])
, which results in an array of a single element which is the original array.
One way to avoid this, is to use a Capture
instead of a Hash
:
my $capture = \( x => 5, y => 20, z => [1, 2] );
my $x = Local::Class.new(|$capture);
say $x; # Local::Class.new(x => 5, y => 20, z => [1, 2])
Or use a Hash
but then de-containerize its values with <>
and turn the whole thing into a Map
(which, unlike a Hash
, won't add back the item containers) before interpolating it into the argument list:
my %hash = x => 5, y => 20, z => [1, 2];
my $x = Local::Class.new(|Map.new: (.key => .value<> for %hash));
say $x; # Local::Class.new(x => 5, y => 20, z => [1, 2])
Using a custom constructor
If you'd rather want to deal with this in the class itself rather than in code that uses the class, you can amend the constructor to your liking.
Note that the default constructor .new
calls .bless
to actually allocate the object, which in turn calls .BUILD
to handle initialization of attributes.
So the easiest way is to keep the default implementation of .new
, but provide a custom .BUILD
. You can map from named arguments to attributes directly in its signature, so the body of the BUILD
routine can actually stay empty:
class Local::Class {
has $.x;
has $.y;
has @.z;
submethod BUILD (:$!x, :$!y, :@!z) { }
}
my %hash = x => 5, y => 20, z => [1, 2];
my $x = Local::Class.new(|%hash);
say $x; # Local::Class.new(x => 5, y => 20, z => [1, 2])
Binding an array-in-an-item-container to a @
parameter automatically removes the item container, so it doesn't suffer from the "array in an array" problem described above.
The downside is that you have to list all public attributes of your class in that BUILD
parameter list. Also, you still have to interpolate the hash using |
in the code that uses the class.
To get around both of those limitations, you can implement a custom .new
like this:
class Local::Class {
has $.x;
has $.y;
has @.z;
method new (%attr) {
self.bless: |Map.new: (.key => .value<> for %attr)
}
}
my %hash = x => 5, y => 20, z => [1, 2];
my $x = Local::Class.new(%hash);
say $x; # Local::Class.new(x => 5, y => 20, z => [1, 2])
%( x => 5, z => [1, 2] )
into\( x => 5, z => [1, 2] )
? How do I turn an already existing hash (like one read from a JSON string, for instance) into a suitable capture? I tried doingLocal::Class.new(%hash.Capture)
, but that results in the same issue you pointed out. – Thremmatology