Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

confusion about lists contained in an aggregate, maybe context problem?

Tags:

raku

Rakudo version 2020.01

I was writing some throw-away code and did not bother to implement a class, just used a Hash as work-alike. I found some surprising behaviour with lists.

class Q1 {}
class R1 {
    has Str $.some-str is required;
    has @.some-list is required;
}

my $r1 = R1.new(
    some-str => '…',
    some-list => (Q1.new, Q1.new, Q1.new)
);

# hash as poor man's class
my $r2 = {
    some-str => '…',
    some-list => (Q1.new, Q1.new, Q1.new)
};

multi sub frob(R1 $r1) {
    for #`(Array) $r1.some-list -> $elem {
        $elem.raku.say;
    }
}

multi sub frob(Hash $r2) {
    for #`(List) $r2<some-list> -> $elem {
        $elem.raku.say;
    }
}

frob $r1;
# OK.
# Q1.new
# Q1.new
# Q1.new

frob $r2;
# got:
# (Q1.new, Q1.new, Q1.new)

# expected:
# Q1.new
# Q1.new
# Q1.new

frob(Hash …) works as expected when I call .flat or .list on the list (even though it is already a list‽).

I tried to make a minimal test case, but this works identical AFAICT.

for [Q1.new, Q1.new, Q1.new] -> $elem {
    $elem.raku.say;
}

for (Q1.new, Q1.new, Q1.new) -> $elem {
    $elem.raku.say;
}

I have read the documentation on List and Scalar several times, but I still cannot make sense out of my observation. Why do I have to special treat the list in the Hash, but not in the class?

like image 786
daxim Avatar asked Feb 04 '20 08:02

daxim


2 Answers

for doesn't loop over itemized values.

When you place something in a scalar container it gets itemized.

sub foo ( $v ) { # itemized
  for $v { .say }
}
sub bar ( \v ) {
  for v { .say }
}

foo (1,2,3);
# (1 2 3)

bar (1,2,3);
# 1
# 2
# 3

An element in a Hash is also a scalar container.

my %h = 'foo' => 'bar';

say %h<foo>.VAR.^name;
# Scalar

So if you place a list into a Hash, it will get itemized.

my %h;

my \list = (1,2,3);
%h<list> = list;

say list.VAR.^name;
# List
say %h<list>.VAR.^name;
# Scalar

So if you want to loop over the values you have to de-itemize it.

%h<list>[]
%h<list><>
%h<list>.list
%h<list>.self

@(%h<list>)

given %h<list> -> @list { … }

my @list := %h<list>;

(my @ := %h<list>)  # inline version of previous example

You could avoid this scalar container by binding instead.

%h<list> := list;

(This prevents the = operator from working on that hash element.)


If you noticed that in the class object you defined it with an @ not $

class R1 {
    has Str $.some-str is required;
    has @.some-list is required;
}

If you changed it to an $ and mark it rw it will work like the Hash example

class R2 {
    has Str $.some-str is required;
    has List $.some-list is required is rw;
}

my $r2 = R2.new(
    some-str => '…',
    some-list => (1,2,3),
);

for $r2.some-list { .say }
# (1 2 3)

It has to be a $ variable or it won't be in a Scalar container.
It also has to be marked rw so that the accessor returns the actual Scalar container rather than the de-itemized value.

like image 75
Brad Gilbert Avatar answered Nov 16 '22 19:11

Brad Gilbert


Not strictly an answer, but an observation: in Raku, it pays to use classes rather than hashes, contrary to Perl:

my %h = a => 42, b => 666;
for ^10000000 { my $a = %h<a> }
say now - INIT now;  # 0.4434793

Using classes and objects:

class A { has $.a; has $.b }
my $h = A.new(a => 42, b => 666);
for ^10000000 { my $a = $h.a }
say now - INIT now;  # 0.368659

Not only is using classes faster, it also prevents you from making typos in initialization if you add the is required trait:

class A { has $.a is required; has $.b is required }
A.new(a => 42, B => 666);
# The attribute '$!b' is required, but you did not provide a value for it.

And it prevents you from making typos when accessing it:

my $a = A.new(a => 42, b => 666);
$a.bb;
# No such method 'bb' for invocant of type 'A'. Did you mean 'b'?
like image 6
Elizabeth Mattijsen Avatar answered Nov 16 '22 19:11

Elizabeth Mattijsen