Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Printing mathematical series concisely in Raku

Tags:

sequence

raku

Mathematical series, take for example the consecutive sequence represented here as an array:

my @seq = my $a=0, {++$a} ... *;
for @seq[^10].kv {state $f=0; ($^k < 4 or $^k > 7) ?? say "a$^k =  "  ~ $^v !! (say "..." if $f ne 1; $f=1) };

Prints:

a0 =  0
a1 =  1
a2 =  2
...

a8 =  8
a9 =  9

1- Is there a simple way to drop just the first element i.e. a0 = 0 from the printed output?

2- Could this code be made more idiomatic?

like image 540
Lars Malmsteen Avatar asked Nov 09 '19 13:11

Lars Malmsteen


4 Answers

my @seq = my $a=0, {++$a} ... *;

my \i = 0;
say( 'a' ~ (i+$_) Z=> (i+$_) ) for @seq[^5]; 

print "------\n";

my \j = 1;
say( 'a'.succ ~ (j+$_) Z=> (j+$_) ) for @seq[^5]; 

Output:

(a0 => 0)
(a1 => 1)
(a2 => 2)
(a3 => 3)
(a4 => 4)
------
(b1 => 1)
(b2 => 2)
(b3 => 3)
(b4 => 4)
(b5 => 5)

I recognize the above doesn't include your 'bespoke' gist ellipsis conditional ( ($^k < 4 or $^k > 7) ), but you seem to have come up with a more elegant way of writing that in the comments. Still (if you don't want to use skip), number keys yourself and include an offset such as i or j indicating how many elements of @seq you wish to skip.

Addendum: Below is an attempt at implementing your 'bespoke' gist ellipsis conditional (using grep):

my @seq = my $a=0, {++$a} ... *;
my \i = 0; my \m = 4; my \n = 7;

do for @seq[^10].grep({4 > $_ or $_ > 7 }) {
  say 'a' ~ (i+$_) Z=> (i+$_); 
  if  $_ == 3 {print "...\n"};
}

Output when \i = 0:

(a0 => 0)
(a1 => 1)
(a2 => 2)
(a3 => 3)
...
(a8 => 8)
(a9 => 9)

Output when \i = 1:

(a1 => 1)
(a2 => 2)
(a3 => 3)
(a4 => 4)
...
(a9 => 9)
(a10 => 10)
like image 112
jubilatious1 Avatar answered Oct 27 '22 15:10

jubilatious1


This might be a bit more idiomatic:

my @seq = 0, *+1 ... *;
say @seq[^4], @seq[7..10]

You don't need to use a lexical variable within the sequence; either Whatever or placeholder variables can safely be used within sequences. Then you can simply select the elements of the sequence you want printed. Which returns «(0 1 2 3)(7 8 9 10)␤»

like image 39
jjmerelo Avatar answered Oct 27 '22 14:10

jjmerelo


You can skip the first N values on any Iterable or Sequence with skip:

for (^5).skip(3) {
    .say
}
# 3
# 4

If you don't specify a number, it will skip only one element.

like image 7
Elizabeth Mattijsen Avatar answered Oct 27 '22 13:10

Elizabeth Mattijsen


A barebones solution

Let's start with a very simple solution for printing a gist of a sequence. It doesn't deal with the specifics you've added to your question but it's a good starting point:

sub seq-range-gist ( @seq ) {
  my @pairs = @seq.pairs;
  join "\n", @pairs.head(3)».gist, '...', @pairs.tail(2)».gist
}

Unlike .kv, which converts its invocant into the form key1, value1, key2, value2, key3, value3, ..., i.e. 6 elements if its invocant contains 3 elements, .pairs converts its invocant into the form key1 => value1, key2 => value2, key3 => value3, ....

I used .pairs instead of .kv partly because it meant I could just use ».gist later on in the code to effortlessly get a nice key1 => value1 display for each element. We'll modify that below but this is a good idiomatic start.

The .head and .tail calls are the idiomatic way to create small lists of the first and last N elements from an invocant list (provided it's not lazy; more about that in a mo).

Given this initial solution, say seq-range-gist (0,1 ... Inf)[^10] displays:

0 => 0
1 => 1
2 => 2
...
8 => 8
9 => 9

Next, we want to be able to "drop just the first element ... from the printed output". Unfortunately say seq-range-gist (0,1 ... Inf)[1..9] displays:

0 => 1
1 => 2
2 => 3
...
7 => 8
8 => 9

We want the number on the left of the => to retain the numbering of the original sequence. To enable this we split the underlying sequence from the range that we want extracted. We add a second parameter/argument @range, and append [@range] to the second line of the sub:

sub seq-range-gist ( @seq, @range ) {
  my @pairs = @seq.pairs[@range];

Now we can write say seq-range-gist (0,1 ... Inf), 1..9 to display:

1 => 1
2 => 2
3 => 3
...
8 => 8
9 => 9

In your question you used the format aINDEX = VALUE rather than INDEX => VALUE. To allow customization of the gist, we add a third &gist routine parameter/argument and invoke that instead of the built in .gist method:

sub seq-range-gist ( @seq, @range, :&gist ) {
  my @pairs = @seq.pairs[@range];
  join "\n", @pairs.head(3)».&gist, '...', @pairs.tail(2)».&gist
}

Note how the "method" invocations in the body of seq-range-gist sub are now .&gist, not .gist. The syntax .&foo invokes a sub &foo (which is typically invoked by writing just foo), passing the invocant on the left of the . as a $_ argument to the sub.

Note also that I've made the &gist parameter a named one by preceding it with a :.

So now say seq-range-gist (0,1 ... Inf), 1..9, gist => { "a{.key} = {.value}" } displays:

a1 =  1
a2 =  2
a3 =  3
...
a8 =  8
a9 =  9

Adding polish

The rest of this answer is bonus material for readers who care about polish.

say seq-range-gist (0, 1, 2, 3), ^3 displays:

0 => 0
1 => 1
2 => 2
...
1 => 1
2 => 2

Oops. And even if there were more pairs than the head and tail combined, so at least we didn't get repeated lines, it'd still be pointless using the head, ..., tail approach to elide just one or two elements. Let's change the last statement in the sub body to eliminate these issues:

  join "\n",
    @pairs < $head + $tail + 3   # Of course, the 3 is a bit arbitrary
      ?? @pairs».&gist
      !! (@pairs.head($head)».&gist, '...', @pairs.tail($tail)».&gist)

Next, it would be nice if the sub did something useful if called without a range or gist. We can mostly fix that by giving the @range and &gist parameters suitable defaults:

sub seq-range-gist (
  @seq,
  @range = @seq.is-lazy ?? ^100 !! ^@seq,
  :&gist = { .gist }
) {

If @seq is not lazy, then @range defaults to the full range of @seq. If @seq is infinite (in which case it's also lazy), then the upto 100 default is fine. But what if @seq is lazy but yields less than 100 defined values? To cover this case we append .grep: *.value.defined to the @pairs declaration:

  my @pairs = @seq.pairs[@range].grep: *.value.defined;

Another simple improvement would be optional head and tail parameters, leading to a final polished solution:

sub seq-range-gist (
  @seq,
  @range = @seq.is-lazy ?? ^100 !! ^@seq,
  :$head = 3,
  :$tail = 2,
  :&gist = { .gist }
) {
  my @pairs = @seq.pairs[@range].grep: *.value.defined;
  join "\n",
    @pairs <= $head + $tail + 2
      ?? @pairs».&gist
      !! (@pairs.head($head)».&gist, '...', @pairs.tail($tail)».&gist)
}
like image 3
raiph Avatar answered Oct 27 '22 13:10

raiph