Multiple Fun with Arrays and Raku

by Arne Sommer

Multiple Fun with Arrays and Raku

[49] Published 26. December 2019

This is my response to the Perl Weekly Challenge #40.

Challenge #40.1: Multiple Arrays Content

You are given two or more arrays. Write a script to display values of each list at a given index.

For example:
Array 1: [ I L O V E Y O U ]
Array 2: [ 2 4 0 3 2 0 1 9 ]
Array 3: [ ! ? £ $ % ^ & * ]
We expect the following output:
I 2 !
L 4 ?
O 0 £
V 3 $
E 2 %
Y 0 ^
O 1 &
U 9 *

The next challenge uses the term «list», and some of the values have more than one character. This challenge uses the term «array», and all the values are exactly one character. It is tempting to assume that the use of «array» is intentional, and assume the traditional C (the programming languange) view of a string; an array of chars. The conclusion is that we support single characters only.

The second issue is the length. Is the three arrays equally long? They are in the given example, but I'll let the program tackle different lengths.

The third issue is the sample arrays. The challenge says «two or more arrays», so I'll support user specified arrays. An unlimited amount of arrays.

Note that I use the expressions «array» and «list» interchangeably in this article. They are not synomyms in Raku (where an array is mutable, and a list is immutable), but that doesn't matter in this article.

File: array-vertical
multi MAIN ()                                                     # [1]
{
  MAIN('I L O V E Y O U', '2 4 0 3 2 0 1 9', '! ? £ $ % ^ & *');  # [1a]
}

multi MAIN (:$verbose, *@strings where @strings.elems)            # [2]
{
  my @arrays = @strings.map(*.words);                             # [3]

  my $length = @arrays>>.elems.max;                               # [4]
  ############ # 4a ## # 4b ## 4c #

  if $verbose                                                     # [5]
  {
    say ":A: { @arrays.perl }";
    say ":L: $length";
  }

  for ^$length -> $index                                          # [6]
  {
    print "{ $_[$index] // ' ' } " for @arrays;                   # [7]
    say "";                                                       # [7a]
  }
}

[1] This «multi MAIN» candidate is used when we run the program without command line arguments. It passes the three default arrays (as strings) to the other candidate [1a].

[2] This «multi MAIN» candidate is used when we run the program with command line arguments. I have used the Slurpy Operator (*) collect all the arguments in an array. The default is to treat them as individual scalar arguments. Note that the slurpy operator also accepts no values, so we have to add a «where» clause to ensure at least one argument (so that the program can choose the correct candidate when run without arguments).

[3] The arrays are specified as strings with embedded spaces, and we use .words to get the individual characters as a list. We apply it to a list (of strings), and uses map to iterate over the strings. The result is a list of list. (And we could have accessed the values with a multi-dimesional lookup like e.g. @arrays[0][1] which gives «L».)

[4] This gives us the maximum number of elements in the lists. We start with the list of lists [4a], applies .elems on all the lists (as set up by >>. instead of the normal . invocation. We have three lists (in our default case), so we get three values in return. And finally we apply .max on that list to the highest value.

[5] See the description of the verbose output below for details.

[6] Iterate over the elemensts, from 0 to the index of the last element (in the largest list). The default lists are equally long.

[7] • Iterate over the lists (with for @arrays) and print the value at the given position for each one. Note the use of the «Defined-or» operator // to avoid using undefined values (when we go past the end of a short list). The more familiar «or» operator || would have replaced the number 0 with a space, and that is wrong. End each row with a newline [7a].

Running it:

$ raku array-vertical
I 2 ! 
L 4 ? 
O 0 £ 
V 3 $ 
E 2 % 
Y 0 ^ 
O 1 & 
U 9 * 

That is spot on.

We can specify the arrays, and enable verbose mode (which only works when we specify the arrays):

$ raku array-vertical --verbose \
  "I L O V E Y O U" "2 4 0 3 2 0 1 9" "! ? £ $ % ^ & *"
:A: [("I", "L", "O", "V", "E", "Y", "O", "U").Seq,
     ("2", "4", "0", "3", "2", "0", "1", "9").Seq,
     ("!", "?", "£", "\$", "\%", "^", "\&", "*").Seq]
:L: 8
I 2 ! 
L 4 ? 
O 0 £ 
V 3 $ 
E 2 % 
Y 0 ^ 
O 1 & 
U 9 * 

Note that the lists are not lists at all, but Sequences. The distinction doesn't really matter now, but it will in the next section.

Fixing the Width

The challenge implies that the values are one character each, but it is easy to add support for longer strings:

File: array-vertical2
multi MAIN ()
{
  MAIN('I L O V E Y O U', '2 4 0 3 2 0 1 9', '! ? £ $ % ^ & *');
}

multi sub MAIN (:$verbose, *@strings where @strings.elems)
{
  my @arrays = @strings.map(*.words.List);  # [3]
  
  my $length = @arrays>>.elems.max; 
  my $width  = @arrays>>.chars>>.max.max;   # [1]
  
  if $verbose
  {
    say ":A: { @arrays.perl }";
    say ":L: $length";
    say ":W: $width";
  }
  
  for ^$length -> $index
  {
    print "{ ($_[$index] // '').fmt("%-{ $width }s") } " for @arrays; say "";
  }                                          # [2]
}

[1] This one deserves an explanation. @arrays>>.chars>> calculates the number of characters for each element, giving us a list of list with the sizes. Applying >>.max reduces that to a list with the highest values in the inner lists. And finally applying .max on that list gives the longest string. We can show it in REPL:

> my @a = [("I", "L", "OV"), ("2", "412", "0"), ("!!!!", "?", "£")]
[(I L OV) (2 412 0) (!!!! ? £)]

> @a>>.chars
[(1 1 2) (1 3 1) (4 1 1)]

> @a>>.chars>>.max
(2 3 4)
 
> @a>>.chars>>.max.max
4

[2] Use the same length (the maximum) on all the strings, so that they come out nicely tabulated. «fmt» is the method form of «sprintf». We specify the width, and the minus sign indicates left justification (adding the spaces at the end).

[3] The program will not work if we keep the inner lists as sequences. A sequence can only be iterated over once (called consumed), and the code in [1] iterates so that [2] will fail. Coercing the sequences to lists fixes that problem.

Running it, with single quotes on the last one to prevent the shell from escaping the «!»s:

$ raku array-vertical2 --verbose "I L OV" "2 412 0" '!!!! ? £'
:A: [("I", "L", "OV"), ("2", "412", "0"), ("!!!!", "?", "£")]
:L: 3
:W: 4
I    2    !!!! 
L    412  ?    
OV   0    £    

The tabulation is correct, but not that good. Let't try with more extreme data:

$ raku array-vertical2 --verbose "I L OV" "2 412 0" '!!!!!!!!!!!!! ? £'
:A: [("I", "L", "OV"), ("2", "412", "0"), ("!!!!!!!!!!!!!", "?", "£")]
:L: 3
:W: 13
I             2             !!!!!!!!!!!!! 
L             412           ?             
OV            0             £             

The width of each column (or input array) should be computed separately. That is easy:

File: array-vertical3

multi MAIN ()
{
  MAIN('I L O V E Y O U', '2 4 0 3 2 0 1 9', '! ? £ $ % ^ & *');
}

multi sub MAIN (:$verbose, *@strings where @strings.elems)
{
  my @arrays = @strings.map(*.words.List);
  
  my $length = @arrays>>.elems.max; 
  my @width  = @arrays>>.chars>>.max; # [1]

  if $verbose
  {
    say ":A: { @arrays.perl }";
    say ":L: $length";
    say ":W: { @width }";             # [1]
  }

  for ^$length -> $index
  {
    my $col = 0;                      # [2]
    for @arrays
    {
      print "{ ($_[$index] // '').fmt("%-{ @width[$col] }s") } "; # [2]
      $col++;                         # [2]
    }
    say "";
  }
}

[1] We drop the final «.max» to keep the list of maximum values (for each list).

[2] use the column number to access the correct maximum element.

Running it:

$ raku array-vertical3 --verbose "I L OV" "2 412 0" '!!!!!!!!!!!!! ? £'
:A: [("I", "L", "OV"), ("2", "412", "0"), ("!!!!!!!!!!!!!", "?", "£")]
:L: 3
:W: 2 3 13
I  2   !!!!!!!!!!!!! 
L  412 ?             
OV 0   £             

That looks much nicer, and still nicer if we drop verbose mode:

$ raku array-vertical3 "I L OV" "2 412 0" '!!!!!!!!!!!!! ? £'
I  2   !!!!!!!!!!!!! 
L  412 ?             
OV 0   £             

The «multi MAIN» concept is nice, but we can manage quite well without it:

File: array-vertical-final
sub MAIN (:$verbose, *@strings)             # [1]
{
  @strings = ('I L O V E Y O U', '2 4 0 3 2 0 1 9', '! ? £ $ % ^ & *')
    unless @strings.elems;                  # [2]
  my @arrays = @strings.map(*.words.List);
  
  my $length = @arrays>>.elems.max; 
  my @width  = @arrays>>.chars>>.max;

  if $verbose
  {
    say ":A: { @arrays.perl }";
    say ":L: $length";
    say ":W: { @width }";
  }

  for ^$length -> $index
  {
    my $col = 0;
    for @arrays
    {
      print "{ ($_[$index] // '').fmt("%-{ @width[$col] }s") } ";
      $col++;
    }
    say "";
  }
}

[1] Just one «MAIN», and I have removed the «where» clause so that it works with, as well as without, arguments.

[2] Use the default values, if none were given on the command line.

Challenge #40.2: Sort SubList

You are given a list of numbers and set of indices belong to the list. Write a script to sort the values belongs to the indices.

For example:
List: [ 10, 4, 1, 8, 12, 3 ]
Indices: 0,2,5
We would sort the values at indices 0, 2 and 5 i.e. 10, 1 and 3.

Final List would look like below:
List: [ 1, 4, 3, 8, 12, 10 ]

This is easy, using array slices.

> my @array   = 10, 4, 1, 8, 12, 3;
> my @indices = 0, 2, 5;
> my @values  = @array[@indices];  # -> (10 1 3)
> my @sorted  = @values.sort;      # -> (1 3 10)

> @array[indices] = @sorted;  # -> (1 3 10)

> say @array; # -> [1 4 3 8 12 10]

An illustration may help:

We can write the code more compact:

> my @array = 10, 4, 1, 8, 12, 3;
> @array[0,2,5].= sort;
> say @array;
[1 4 3 8 12 10]

The program should be easy to understand now. Note the named variables used to override the list of values and the list of indices, both as strings.

@array[0,2,5].= sort is short for @array[0,2,5] = @array[0,2,5].sort. This construct can be used on most operators so that they assign the new value back to the original variable. (E.g. $a += 10 which is the same as $a = $a + 10.)

File: array-partialsort
sub MAIN (:$verbose, :$list = "10 4 1 8 12 3", :$sort = "0 2 5")
{
  my @array   = $list.words;
  my @indices = $sort.words;

  if $verbose
  {
    say ":A: @array[]";
    say ":I: @indices[]";
    say ":O: @array[@indices]";
    say ":R: { @array[@indices].=sort }";
  }
  else
  {
    @array[@indices].=sort;
  }

  say @array;
}

Running it:

$ raku array-partialsort
[1 4 10 8 12 3]

That is not what we should expect. The challenge says «1 4 3 8 12 10».

We can use verbose mode to see what is going on:

$ raku array-partialsort --verbose
:A: 10 4 1 8 12 3
:I: 0 2 5
:O: 10 1 3
:R: 1 10 3
[1 4 10 8 12 3]

The «O» (Original) line is before sorting, and «R» (Replacement) is after. The sort order is wrong. Raku has decided to sort the values as strings (where «10» comes before «2»). The reason is that the values really are strings (as «.words» gives strings). The solution is to ensure numbers:

File: array-partialsort (changes only)
  my @array   = $list.words>>.Numeric;

Now we get the correct result:

$ raku array-partialsort
[1 4 3 8 12 10]

We can try with user specified values:

$ raku array-partialsort --verbose --list="1 2 3 4 5 6 7 8 9 10 11" --sort="9 1 5"
:A: 1 2 3 4 5 6 7 8 9 10 11
:I: 9 1 5
:O: 10 2 6
:R: 2 6 10
[1 6 3 4 5 10 7 8 9 2 11]

Looking good.

And that's it.