In Perl5 you can do something like this:
#!/usr/bin/env perl
use 5.010;
package Local::Class {
use Moo;
has [qw( x y )] => ( is => 'ro');
sub BUILDARGS { shift; return (@_) ? (@_ > 1) ? { @_ } : shift : {} }
}
use Local::Class;
# Create object directly
my $x = Local::Class->new( x => 1, y => 10 );
say $x->x, ' ', $x->y; # 1 10
# Arguments from a hash
my %hash = ( x => 5, y => 20 );
$x = Local::Class->new(%hash);
say $x->x, ' ', $x->y; # 5 20
# Arguments from a hash reference
$x = Local::Class->new(\%hash);
say $x->x, ' ', $x->y; # 5 20
The two calls in the bottom work the same because of the custom BUILDARGS
method, which basically turns them both into the sort of hash references expected by Moo(se)?.
But how can I do the same in Perl6?
#!/usr/bin/env perl6
class Local::Class {
has $.x;
has $.y;
}
my $x;
# This works
$x = Local::Class.new( x => 1, y => 10 );
say $x.x, ' ', $x.y; # 1 10
# This doesn't
my %hash = %( x => 5, y => 20 );
$x = Local::Class.new(%hash);
# This doesn't either
$x = Local::Class.new(item(%hash));
# Both die with:
# Default constructor for 'Local::Class' only takes named arguments
So how can I take a hash that has been created elsewhere, and convert it into the sort of named arguments needed by the default constructor of a class?
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])
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])
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