skip to Main Content

Trying to progressively convert some Perl scripts to Raku. I am quite stuck with the following one, even after browsing quite a lot here and reading Learning Perl 6 more deeply.

The part on which I can’t make progress is the last loop (converted to for); getting keys and sorting them by month name and day number looks impossible, but I am sure it is doable.

Any hints on how to achieve this with "idiomatic" syntax would be really welcome.

#!/usr/bin/perl

use strict;

my %totals;

while (<>) {
    if (/redis/ and /Partial/) {
        my($f1, $f2) = split(' ');
        my $w = $f1 . ' ' . $f2;
        $totals{$w}++;
    }
}

my %m = ("jan" => 1, "feb" => 2, "mar" => 3, "apr" => 4, "may" => 5, "jun" => 6,
         "jul" => 7, "aug" => 8, "sep" => 9, "oct" => 10, "nov" => 11, "dec" => 12);

foreach my $e (sort { my($a1, $a2) = split(' ', $a) ; my($b1, $b2) = split(' ', $b) ;
            $m{lc $a1} <=> $m{lc $b1} or $a2 <=> $b2 } keys %totals) {
    print "$e", " ", $totals{$e}, "n";
}

5

Answers


  1. You could try something like:

    enum Month (jan => 1, |<feb mar apr may jun jul aug sep oct nov dec>);
    
    lines()
    andthen .grep: /redis/&/Partial/
    andthen .map: *.words
    andthen .map: {Month::{.[0].lc} => .[1].Int} 
    #or andthen .map: {Date.new: year => Date.today.year, month =>  Month::{.[0].lc},  day => .[1], }
    andthen  bag $_
    andthen .sort
    andthen .map: *.put;
    
    Login or Signup to reply.
  2. Fed with the same sample data, your perl code produces the same output as this.

    my $data = q:to/END/; 
    may 01 xxx3.1 Partial redis
    may 01 xxx3.2 Partial redis
    may 01 xxx3.3 Partial redis
    apr 22 xxx2.2 Partial redis
    apr 22 xxx2.1 Partial redis
    mar 01 xxx1 redis Partial
    some multi-line
    string
    END
    
    
    sub sort-by( $value )
    {
      state %m = <jan feb  mar apr may jun jul aug sep oct nov dec> Z=> 1..12;
      %m{ .[0].lc }, .[1] with $value.key.words;
    }
    
    say .key, ' ', .value.elems 
      for $data
        .lines
        .grep( /redis/ & /Partial/ )
        .classify( *.words[0..1].Str )
        .sort( &sort-by );
    
    Login or Signup to reply.
  3. I think that this is close to what you are asking for… also shows that perl6/raku is quite closely related to perl5 unless you want to get fancy…

    #test data...
    my %totals = %( 
        "jan 2" => 3,
        "jan 4" => 1,
        "feb 7" => 1,
    );
    
    my %m = %("jan" => 1, "feb" => 2, "mar" => 3, "apr" => 4, "may" => 5, "jun" => 6,
             "jul" => 7, "aug" => 8, "sep" => 9, "oct" => 10, "nov" => 11, "dec" => 12);
    
    my &sorter = { 
        my ($a1, $a2) = split(' ', $^a); 
        my ($b1, $b2) = split(' ', $^b);
        %m{lc $a1} <=> %m{lc $b1} or $a2 <=> $b2 
    }
    
    for %totals.keys.sort(&sorter) ->$e {
        say "$e => {%totals{$e}}" 
    }
    
    #output
    jan 2 => 3
    jan 4 => 1
    feb 7 => 1
    

    The main changes are:

    • %totals{$e} for $totals{$e}
    • %() instead of {} for hash literals
    • for with method syntax and -> instead of foreach with sub syntax
    • $^a and $^b in sort routine need caret twigils (^)
    • say is a bit cleaner than print
    Login or Signup to reply.
  4. TL;DR @wamba provides an idiomatic solution. This answer is a minimal "mechanical" translation instead.

    I think your question and this answer suggests that a great way to learn many of Raku’s basics as they relate to Perl is:

    1. Feed a small Perl program into Rakudo;

    2. Methodically investigate/fix each reported error until it works;

    3. Post a question to StackOverflow if you get stuck.

    Presuming that’s what you did, great. If not, hopefully this answer will inspire you or other readers to try doing just that. 😍

    The code

    my %totals;
    
    for lines() {
        if (/redis/ and /Partial/) {
            my ($f1, $f2) = split(' ', $_);
            my $w = $f1 ~ ' ' ~ $f2;
            %totals{$w}++;
        }
    }
    
    my %m = ("jan" => 1, "feb" => 2, "mar" => 3, "apr" => 4, "may" => 5, "jun" => 6,
             "jul" => 7, "aug" => 8, "sep" => 9, "oct" => 10, "nov" => 11, "dec" => 12);
    
    for sort { my ($a1, $a2) = split(' ', $^a) ; my ($b1, $b2) = split(' ', $^b) ;
                %m{lc $a1} <=> %m{lc $b1} or $a2 <=> $b2 }, keys %totals
        -> $e {
        print "$e", " ", %totals{$e}, "n";
    }
    

    works for the following test input:

    feb 1 redis Partial
    jan 2 Partial redis
    jan 2 redis Partial
    

    The mechanical translation process

    I began properly working on your question by feeding the code in your question to Rakudo. And was surprised to get Unsupported use of <>. .... It was Perl code, not Rakunian! πŸ™ƒ

    Then I saw @wamba had provided an idiomatic solution. I decided I’d do the most direct translation possible instead. My first attempt worked. Order restored. πŸ™‚

    I pondered how best to explain my changes. I wondered what the error messages would be if I went back to the start and just fixed one at a time. The result was a delightful series of good error messages. So I’ve structured the rest of this answer as a series of error messages/fixes/discussions, each one leading to the next, until the program just works.

    In the interests of simplicity I drop most of the info from the error messages. The messages/fixes are in the order I encountered them by fixing one at a time:


    1. Unsupported use of <>.  In Raku please use: lines() to read input ...
      ------> while (<⏏>) {
      

      (⏏ is Unicode’s eject symbol, marking the point where the compiler conceptually "ejects" the code.)

      The idiomatic replacement of Perl’s while (<>) is for lines().


    1. Variable '$f1' is not declared
      ------>         my(⏏$f1, $f2) ...
      

      Raku interprets code of the form foo(...) as a function call if it’s where a function call makes sense. This takes priority over interpreting foo as a keyword (i.e. my as a variable declarator).

      Next, because my($f1, $f2) is interpreted as a function call, the $f1 is interpreted as an argument that you haven’t declared, leading to the error message.

      Inserting whitespace after the my fixes both the real problem and this apparent one.

      (This error occurred in multiple locations in your code; I applied the same fix each time.)


    1. Unsupported use of .  to concatenate strings.  In Raku please use: ~.
      ------>         my $w = $f1 .⏏ ' ' . $f2;
      

      To help remember that ~ is used as a string operation in Raku, note that it looks like a piece of string. 🧡


    1. Variable '$totals' is not declared.  Did you mean '%totals'?
      ------>         ⏏$totals{$w}++;
      
      • As Damian Conway notes, "We took this Perl table of what do you use when, and we made it this table instead".

      • The code $totals{...} is syntactically valid. One can bind or assign a hash (reference) to a scalar. But Rakudo (the Raku compiler) knows at compile time that the code hasn’t declared a $totals variable, so it rightly complains.

      • Your code has declared a %totals variable, so Rakudo helpfully asks if you meant that.

      (This error occurred in multiple locations in your code; I applied the same fix each time.)


    1. Unsupported use of 'foreach'.  In Raku please use: 'for'.
      ------> foreach⏏ my 
      

      Raku code tends to be shorter (and more readable) than Perl code. It’s mostly due to design that goes beyond mere paint, but little things like s/for/foreach don’t hurt.


    1. This appears to be Perl code
      ------> for ⏏my $e 
      

      This error message is arguably LTA (I’m thinking "non-descriptive"). But it’s equally arguably pretty good, all things considered.

      Perl and Raku support binding of a value to a new lexical variable/parameter scoped to a block. Perl uses my, and puts the variable(s) in front of the value(s). Raku puts the value(s) first, inserts a -> between them and any variables, and skips the my.

      (There’s a good deal of richness to this use of -> that I’ll not get into here because it doesn’t matter for this example. But it’s worth being aware that this change buys Rakoons a good deal, and you’ve got that to look forward to.)


    1. Variable '$a' is not declared
      ------> for sort { my ($a1, $a2) = split(' ', ⏏$a)
      

      As Perl devs know, it has special variables $a and $b.

      Raku generalized this notion to $^foo parameters, a convenient DRY way to add positional parameters to a closure while skipping the usual ceremony in which one has to specify the name twice (once to declare it, another time to use it).

      An unusual aspect of these that might initially seem crazy, but is actually very ergonomic, is that their formal parameter position is determined by their name. So, given $^foo and $^bar, the latter will bind to the first positional argument, $^foo to the second.


    1. Missing comma after block argument to sort
      ------> { ... }⏏ keys
      

      I inserted a comma where indicated.


    1. Calling split(Str) will never work with signature of the proto ($, $, |)
      ------>         my ($f1, $f2) = ⏏split(' '
      

      Some Perl routines implicitly presume use of $_. There’s no syntactic way to know whether or not any given routine is implicitly using it. You just have to read each routine’s definition. Raku dropped that.

      So Rakudo concludes the split routine is missing the string that’s to be split.

      (The | in the "signature of the proto ($, $, |)" just means "other arguments that can optionally be passed", so you can ignore that. The $, $ indicates two arguments are required, so we’re missing one.)

      A quick check of the routine definition shows the sub version of split requires the string to be split as its second positional argument. Thus I switched to split(' ', $_).


    1. The code works. o/

      Notably, all the actual error messages started with ===SORRY!=== Error while compiling. That means they were all caught before the program even ran, which is nice. 😎

    Login or Signup to reply.
  5. You’ve already got good answers, but I’m taking the opportunity to expose you to some other standard Raku tools and idioms that seemed natural to me for your problem.

    For both my solutions:

    • My equivalents of your %totals variable store keys in a structured data form rather than just as string keys. The supposed rationale is to simplify the sort and presentation. (But really it’s to show you another way. It would of course be trivial to ensure the month and day numbers are concatenated as two two digit numbers to ensure correct sorting.) I use two different key types to show variations on this theme.

    • I deal with conversion to/from month names by constructing hashes mapping names to numbers. I declare one with the .pairs or .antipairs method, and then apply the reverse to convert in the other direction. I do this one way in the first solution and the other in the second. And I set the number for jan to 0 in one solution and 1 in the other.

    Short and sweet, lean on Pairs

    When declaring a %foo variable, if you don’t specify its key type, it defaults to Str. But in this code, the key of each Pair in %totals is itself a Pair:

    my %totals{Pair}; # Keys of the `Pair`s in `%totals` are themselves `Pair`s
    
    my %months = <jan feb mar apr may jun jul aug sep oct nov dec> .pairs; # 0 => jan
    
    for lines.grep(/redis/ & /Partial/)Β».words {
      ++%totals{ %months.antipairs.hash{ lc .[0] }.Int => .[1].Int }
    }
    
    for %totals .sort {
      printf "%3s %2d : %-dn", %months{.key.key}, .key.value, .value
    }
    

    If no sort closure(s) are specified, Raku’s sort routine, when applied to a hash, sorts its entries by comparing their keys using cmp. Furthermore, for an ordinary hash, comparing two keys means comparing two strings.

    That would work fine for your situation if these strings were each date’s month and day formatted as two digits each and then concatenated. Alternatively, splitting and schwartzian works fine too. Raku’s really good at that stuff but I preferred to go a different way with this answer, so that the default sort did the right thing.

    For this first solution, I picked Pairs as the key type. When cmp compares Pairs, it sorts first by key and then by value within that. Both key and value were coerced to Ints, thus the above code correctly sorts by month, then days within that.

    More structure, use Dates

    This version adds structure and more fancy typing. It wraps the equivalent of the %totals hash (renamed %.data) into an outer object containing some utility routines, and makes the inner key object be a Date instead of a Pair:

    role Totals {
    
      my %months = <jan feb mar apr may jun jul aug sep oct nov dec> .antipairs Β«+Β» 1; # jan => 1
      method month-name (Int $num --> Str)  { %months.antipairs.hash{$num} }
      method month-num  (Str $name --> Int) { %months{lc $name} }
    
      has %.data{Date} handles <sort>;
    
    }
    
    my $totals = Totals.new; 
    
    for lines.grep(/redis/ & /Partial/)Β».words {
      ++$totals.data{ Date.new: :year(2000), :month(Totals.month-num: .[0]), :day(.[1]) }
    }
    
    for $totals .sort {
      printf "%3s %2d : %-dn", Totals.month-name(.key.month), .key.day, .value
    }
    

    In the first solution, sort did the right thing because it was comparing Pairs, and cmp in turn did the right thing given how I’d set the pairs up.

    In this solution sort/cmp do the right thing without needing to coerce string values to Ints, because the totals entries are Dates and they compare according to ordinary date comparison rules.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search