Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I construct an object in Perl6 from a hash?

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?

like image 741
jja Avatar asked Dec 01 '16 00:12

jja


1 Answers

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])
like image 190
smls Avatar answered Sep 20 '22 23:09

smls