Perl 6: difference between Array
s and List
s
Perl 6 does support two different type of
array-like iterables:
[Array](https://docs.perl6.org/type/Array)
and
[List](https://docs.perl6.org/type/List)
.
While both tend to have an uniform, iterable like behavior, they serve different scopes and in this article I would like to point out some of differences.
Array
s are arrays!
That’s a simple rule: if you use a
@
sigil you are instrumenting the compiler to place an
[Array](https://docs.perl6.org/type/Array)
as container.
An Array
is a sequence of containers: an array does contains one container in every specific position, even when you construct multidimensional arrays.
That’s said, the following piece of code is not surprising:
my @array = 'a' .. 'c';
@array.say;
say 'Container: ' ~ @array.VAR.^name;
say 'Name: ' ~ @array.^name;
producing as output
[a b c]
Container: Array
Name: Array
So what do we have here? A
@
sigil provides an
Array
container. It’s so simple!
There’s another simple rule:
arrays are built depending on commas ,
, not on parentheses. In other words, parentheses are used only to group commas into multidimensional arrays. The following example shows how
@array
stays mono-dimensional, while
@m_array
is not:
my @array = 'a', 'b', 'X', 'Y', 'Z';
@array.say;
say 'Container: ' ~ @array.VAR.^name;
say 'Name: ' ~ @array.^name;
my @m_array = ( 'a', 'b' ), ( 'X', 'Y', 'Z' );
@m_array.say;
say 'Container: ' ~ @m_array.VAR.^name;
say 'Name: ' ~ @m_array.^name;
that produces
[a b X Y Z]
Container: Array
Name: Array
[(a b) (X Y Z)]
Container: Array
Name: Array
As readers can see, the
@m_array
is really made up of two arrays, and it is even simpler to see this trying to iterate over the latter:
my @m_array = ( 'a', 'b' ), ( 'X', 'Y', 'Z' );
for @m_array -> @a {
@a.say;
say 'Container: ' ~ @a.VAR.^name;
say 'Name: ' ~ @a.^name;
}
(a b)
Container: List
Name: List
(X Y Z)
Container: List
Name: List
and here comes the
List
: remember that
every element of an array is a container, so a multidimensional array cannot be made by the union of literal arrays, but must be an array containing single containers that point to values. This is achieved by
[List](https://docs.perl6.org/type/List)
.
List
s are scalar iterable containers
Well,
[Lists](https://docs.perl6.org/type/List)
are objects and therefore can be assigned to a
Scalar
container.
List
s can be iterated and sliced as an
Array
, so they
taste as arrays. In contrast to
Array
s, the
List
s can be
lazy and can be assigned to a
Scalar
container.
Let’s start with a simple example:
my @array = 'a', 'b', 'X', 'Y', 'Z';
my @array_uc = @array.map( *.uc );
my $list = ( @array, @array_uc );
$list.say;
say 'Container: ' ~ $list.VAR.^name;
say 'Name: ' ~ $list.^name;
the above code produces the following output:
([a b X Y Z] [A B X Y Z])
Container: Scalar
Name: List
As readers can see, the
$list
variable is of course a
Scalar
but holds a
List
and delegates to it. The
List
in turn, has been made by doubling the
@array
array, therefore it ends up holding a
List
made by two
Array
s.
This can be clearly seen iterating over the scalar (please note:
iterating over a scalar):
for $list.list -> @a {
@a.say;
say 'Container: ' ~ @a.VAR.^name;
say 'Name: ' ~ @a.^name;
}
that produces a very similar output for
both iterations, expliciting saying that the
List
is made by two
Array
s:
[a b X Y Z]
Container: Array
Name: Array
[A B X Y Z]
Container: Array
Name: Array
It is interesting to note that a slight change in the above code produces a totally different result:
my @array = 'a', 'b', 'X', 'Y', 'Z';
# my @array_uc = @array.map( *.uc );
my $list = ( @array, @array.map( *.uc ) );
$list.say;
say 'Container: ' ~ $list.VAR.^name;
say 'Name: ' ~ $list.^name;
with the adoption of
map
within the list construction the result is:
[a b X Y Z]
Container: Array
Name: Array
(A B X Y Z)
Container: List
Name: List
and that’s means that the second element of the
$list
is, in turn, a
List
. Usually this is not perceived at all, since
List
s and
Array
s are iterable in the very same way, as well as slicing works the same.
Of course, being
$list
a scalar, it could seem awkward to write something like
$list[0][1]
, but it does what it supposed to do (i.e., accessing the ‘b’), as well as
$list[1][1]
(i.e., accessing the ‘B’).
List
s are immutable, except when they are mutable!
In the previous example,
$list[1]
is a
List
made up by literals. What happens if one of those elements is modified by indexing?
There’s an error:
$list[1][1] = 'C';
# Cannot modify an immutable Str (B)
but the same does not happen if an array element is changed on the fly.
That is because, again,
an Array
forces a container on each element, while
List
does not.
This is clearly shown introspecting the elements:
say "Element is $list[0][1] of type "
~ $list[0][1].^name ~ ' container is '
~ $list[0][1].VAR.^name;
say "Element is $list[1][1] of type "
~ $list[1][1].^name ~ ' container is '
~ $list[1][1].VAR.^name;
that produces as output:
Element is b of type Str container is Scalar
Element is B of type Str container is Str
So, for short,
B
is not modifiable, at least not directly.
How to proceed then?
A List
element can be modified if it contains a container.
Even the
is copy
on iteration will not produce the desired effect, consider the following:
for $list.list[1][0] -> $elem is copy {
$elem = 'CHANGED!' ~ $elem;
say "Element is [$elem] of type "
~ $elem.^name ~ ' container is '
~ $elem.VAR.^name;
}
say "Element is $list[1][1] of type "
~ $list[1][1].^name ~ ' container is '
~ $list[1][1].VAR.^name;
what happens is that the
$elem
is changed within its scope, that is the
for
loop, but changes are not pushed back to the list.
The only chance is to declare the list as made of containers:
my $first = 'a';
my $second = 'b';
$list = ( ( $first, $second ), @array );
say "Element is $list[1][1] of type " ~ $list[1][1].^name ~ ' container is ' ~ $list[1][1].VAR.^name;
$list[1][1] = 'C';
say "Element is $list[1][1] of type " ~ $list[1][1].^name ~ ' container is ' ~ $list[1][1].VAR.^name;
in this way the list is made by a first list with two
Scalar
containers within it, and that means it is possible to change them on the fly.
So the rule of thumb is that the
List
itself is immutable (number of elements), but its values can be mutable as far as they are contained into a container.
Building a List
one item at a time
Perl 6 prodives the special
[gather take](https://docs.perl6.org/syntax/gather%20take)
control flow to build up a list one element at a time.
The idea is simple: each block of code that will produce a new item uses the
take
return-like statement,
take
instruments the otuer list to accept the new item as it
gather
.
The following short example shows the concept in a clear way:
sub produce_one_element { take ('a' .. 'z').pick; }
my @list = gather produce_one_element for 1..5;
@list.say;
# [c s q e w]
the
@list
is appended one element at a time via the
gather
control flow. Please consider that the following code will not achieve the same effect, since the
for
would be evaluated first and a single assignment is performed against the array:
sub produce_one_element { return ('a' .. 'z').pick; }
my @list = produce_one_element for 1..5;
@list.say;
# [m]
The
gather take
control flow allows also the
lazy
keyword for letting a list to be lazily populated. This is simply shown with the following code:
sub produce_five_elements {
say 'Generating first element';
take ('a' .. 'z').pick;
say 'Generating second element';
take ('a' .. 'z').pick;
say 'Generating third element';
take ('a' .. 'z').pick;
say 'Generating fourth element';
take ('a' .. 'z').pick;
say 'Generating fifth element';
take ('a' .. 'z').pick;
}
@list = lazy gather produce_five_elements;
@list[0].say;
@list[4].say;
what happens is that the
produce_five_elements
will produce one letter each time it is called, yelding the outer scope control flow after each call. This means that once it has been defined the
@list
will be empty, and once it is accessed an element, several calls to satisfy such index will be performed.
In other words, the output is the following:
Generating first element
o
Generating second element
Generating third element
Generating fourth element
Generating fifth element
w
As readers can see, after the first
@list[0]
access a single call to the method is performed, while after the access to the
@list[4]
all the previous items will be filled by a single method call.
It is important to note that:
- each
take
must be performed to fill each index access, otherwise the Any
instance will be placed in the case an index greater than the take
-ing;
- the code block cannot be used without a
gather
, otherwise the compiler will respond with a take without gather
.
The
gather take
control flow produces an
Array
container when assigned to a
@
sigil variable, but produces a
Seq
when assigned to a
Scalar
.
Sequences
There is another special
List
case in Perl 6:
[Seq](https://docs.perl6.org/type/Seq)
(namely
sequences).
A sequence is a
List
not fully populated, so something that is lazily filled (e.g., via
lazy gather take
as shown above).
A
Seq
does provide a partial iterable interface, and therefore cannot be bound to a
@
variable:
my @seq = Seq.new( <1, 2, 3> );
will produce an exception as
expected Iterator but got List
.
In particular
Seq
does not keep around values once they have been accessed, so that the memory occupation does not grow linearly depending on the number of elements in the sequence. This is particular sueful when iterating thru the lines of a file.
Flattening arrays and lists
The particular
[Slip](https://docs.perl6.org/type/Slip)
class is a subclass of
List
and allows for the latter to become flat.
A flat list is a list without a multidimensional structure.
This means that using the
|
slip prefix operator the following
@list
will be made only by six elements, not two lists of three elements:
@list = ( |(1, 2, 3), |(9, 8, 7) );
@list.say;
# [1 2 3 9 8 7]
In the case of a
gather take
control flow it is required to use the
slip()
method to flat the elements returned into a flat list:
sub produce_three_elements {
take slip (1, 2);
take slip (4, 5);
take slip (5, 6);
}
@list = gather produce_three_elements;
@list.say;
# [1 2 4 5 5 6]
without the
slip()
method call on each
take
the result would have been three separated lists as in
[(1 2) (4 5) (5 6)]
.
Emptying an Array
The special class
Empty
is a
slip of an empty list, and assigning
Empty
to an array element makes it, well, empty! The same happens to assign the empty list
()
to a position:
my @array = 1,2,3,4;
@array[0] = 'FLUCA';
@array[1] = Empty;
@array[2] = ();
@array.say;
that produces
[FLUCA () () 4]
.
Assigning
Empty
to the full array makes it totally empty, that is deletes all the elements. The same happens for
List
, with the execption that an empty list becomes immutable, while an empty array can be modified.
Conclusions
Perl 6 offers different iterable containers, mainly
Array
and
List
, the former are used as
collection of containers for
@
variables, while the latter is used for
Scalar
containers and could be lazy populated and immutable.
In the case a
List
is lazily generated via a
lazy gather take
control flow, the
List
can mutate into a
Seq
.
Arrays are mutable, since they force the containers on each element, while other itarable objects are immutable; it is still possible to change the content of a cell if it holds a container (
Scalar
). It is important to note that
Seq
does not implement the full iterable semantic, and therefore cannot be assigned to a
@
variable.