Perl 6 Int Erval

by Arne Sommer

Perl 6 Int-Erval

Published 4. May 2019

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

Challenge #1

«Create a script which takes a list of numbers from command line and print the same in the compact form. For example, if you pass “1,2,3,4,9,10,14,15,16” then it should print the compact form like “1-4,9,10,14-16”.»

Getting the individual numbers from the command line is easy, as the values are comma separated. Applying «.split(",")» on the string gives a list of the values:

> say "1,2,3,4,5,6,dd,wqqw,ao,1.1".split(",").perl;
("1", "2", "3", "4", "5", "6", "dd", "wqqw", "ao", "1.1").Seq

See docs.perl6.org/routine/split for more information about «split».

File: int-erval (partial)
unit sub MAIN (Str $values);

my @values = $values.split(",");

Error checking on the values is a good thing. We'll start by ensuring that the values are integers. The built-in «is-int» method doesn't work, as it is defined for Ranges only. But we can use the «Int» method:

die "Integers only!" unless all(@values>>.Int);

Note the hyper method call to «Int», so that it is done on each element in the array. The «all» wrapper ensures that the condition must be true for all of them, and «unless» turns it upside down; one single non-integer value causes the program to die.

Now, this dies if we give it a letter. But a number with a decimal point and a fractional part isn't stopped, as the value can be coerced (converted) to an integer (albeit by truncating the value). So I'll have another go with a Regex; «\D» matches non-digits, so it dies if we have any non-digit in the list:

die "Integers only!" if @values.grep(/\D/);

We can also make sure that the integers come in correct order:

die "Wrong order!" unless [<] @values;

The Reduction metaoperator [] applies the operator (given in the brackets) between the values in the list after it. A more familiar example is «[*] (1 .. 10)» to calculate «10!» (10 faculty), but any operator will do.

See docs.perl6.org/language/operators#Reduction_metaoperators for more information about Reduction metaoperators.

File: int-erval
unit sub MAIN (Str $values);

my @values = $values.split(",");

die "Integers only!" if @values.grep(/\D/);
die "Wrong order!"   unless [<] @values;

my @result;

my @current = @values.shift;        # [1]

while @values                       # [2]
{
  my $next = @values.shift;         # [2]
  if $next == @current[* -1] + 1
  {
    @current.push($next);           # [3]
  }
  else
  {
    @result.push(fix-it(@current)); # [4]
    @current = $next;               # [4]
  }
}

@result.push(fix-it(@current)) if @current.elems;   # [5]

say @result.join(",");                              # [6]

sub fix-it (@list)                                  # [7]
{
  return @list[0]              if @list.elems == 1; # [8]
  return "@list[0],@list[1]"   if @list.elems == 2; # [9]
  return "@list[0]-@list[*-1]" if @list.elems  > 2; # [10]
}

[1] We start with the first value (in @current).

[2] And continue as long as there are more values in the list (placed in $next).

[3] If the next value is one after the previuos one (the next integer value), we add it to @current.

[4] If not, the @current array is added to the @result list. And we reset the @current array.

[5] Finally (when we run out of new values), we add the last ones to the @result list.

[6] Add a comma between the value groups.

[7] This procedure fixes a list of (consecutive) integers;

[8] if one integer, print it.

[9] if two integers, print them with a comma in between.

[10] if more than two integers, print the first and last with a dash in between.

A gather/take Alternative

It is possible to get rid of the «@result» array by using «gather/take» instead:

File: int-erval-gather
unit sub MAIN (Str $values);

my @values = $values.split(",");

die "Integers only!" if @values.grep(/\D/);
die "Wrong order!"   unless [<] @values;

my $result := gather                       # [1]
{
  my @current = @values.shift;
  
  while @values
  {
    my $next = @values.shift;
    if $next == @current[* -1] + 1
    {
      @current.push($next);
    }
    else
    {
      take fix-it(@current);               # [2]
      @current = $next;
    }
  }

  take fix-it(@current) if @current.elems; # [3]
}

say $result.join(",");                     # [4]

multi sub fix-it (@list where @list.elems == 1)  # [5]
{
  return @list[0];
}

multi sub fix-it (@list where @list.elems == 2)  # [5]
{
  return "@list[0],@list[1]";
}

multi sub fix-it (@list where @list.elems  > 2)  # [5]
{
  return "@list[0]-@list[*-1]";
}

[1] We use «gather» (with binding; «:=») to set up the supply of values, as a lazy data structure (where the values are only calculated when actually needed).

[2] We use «take» to deliver a value to the consuming part of the program.

[3] A final «take», if we have any values left in the pipeline.

[4] Calling «$result» like this fetches all the values at the same time.

[5] I have chosen Multiple dispatch with «multi sub» and different signatures instead of the «if» conditions in the original version.

See my Perl 6 gather, I take article for more information about gather/take.

See docs.perl6.org/syntax/multi for more information about «multi» and Multiple dispatch.

«int-erval-gather» is essentially the same program as «int-erval», but it scores higher on the show off index.

Challenge #2

«Create a script to calculate Ramanujan’s constant with at least 32 digits of precision. Find out more about it here

A Rational Digression

In my Perl 6 P(i)ermutations article I showed that we got rounding errors when we have too many digits after a decimal point (in the «Fixing Pi» section, with indentation added to make it easier to spot the difference):

> 3.1415926535897932384626433832795028841971693993751
  3.141592653589793221627946859855630841326670589198336

It turns out that we can use the «FatRat» type to avoid the rounding error. But this type must be requested explicitly:

> 3.1415926535897932384626433832795028841971693993751.FatRat
  3.1415926535897932384626433832795028841971693993751

The «Rat» type represents a Rational Number, with 64 bits used for the two parts (the numerator and the denomirator). If we need larger numbers, the «FatRat» type is the answer. It has no limits on the size of the two parts, but is slower. It is not used by default, butc we must be explictly requested, as done above.

See docs.perl6.org/type/Rat for more information about «Rat».

See docs.perl6.org/type/FatRat for more information about «FatRat».

The obvious showoff value for «Rat» is «1/3», which is impossible to represent as a normal number without a rounding error:

> say 1/3 + 1/3 + 1/3;  # -> 1
> say (1/3).WHAT;       # -> (Rat)
> say (1/3).perl;       # -> <1/3>

But it can also hold finite values, as done above.

Ramanujan, First Try

Just programming ahead with the definition gives us a floating point number (Num), with limited precission (using 64-bit, defined by IEEE 754 but better known as "double precision"):

> my $a = e ** (pi * sqrt(163));  # -> 2.625374126407677e+17
> $a.WHAT;                        # -> (Num)

See docs.perl6.org/type/Num for more information about «Num».

Just coercing the result to «FatRat» doesn't work out, as the fractional part is missing:

> say (e ** (pi * sqrt(163))).FatRat;  # -> 262537412640767712

Coercing a «Num» to a «FatRat» can actually make it loose precision, as shown here with «pi»:

> say pi;              # -> 3.141592653589793
> say pi.FatRat;       # -> 3.141593
> say pi.FatRat.perl;  # -> FatRat.new(355, 113)
> say 355/113;         # -> 3.141593

The «Rat::Precise» module «stringifies Rats to a configurable precision» and may be useful. Let us test it:

> use Rat::Precise;
> say pi.FatRat.precise;  # -> 3.14159292035398230088495575221239
> say pi;                 # -> 3.141592653589793
#                              3.141592653589793238462643383279502884197169...

The difference is quite significant! According to the experts (whose value is shown on the third line; at least partial as they present pi with a million digits), the unmodified «pi» is right. So the conclusion is that «Rat::Precise» isn't very precise.

The square root in the Ramanujan Constant is a victim of the same problem:

> say sqrt(163);                # -> 12.767145334803704
> say sqrt(163).FatRat;         # -> 12.767145
> say sqrt(163.FatRat);         # -> 12.767145334803704
> say sqrt(163.FatRat).FatRat;  # -> 12.767145

Conclusion: This isn't going to work out.

Ramanujan, Second Try

What we need is a way of computing the value, as a «FatRat», without loosing precision along the way.

Which operators cause problems (by truncating the value) is above my pay grade, but we can use an operator that clearly isn't affected; assignment:

File: ramanujan (with a newline added to make it fit the screen)
my FatRat $ramanujan = 262537412640768743.9999999999992500725971981856888\
793538563373369908627075374103782106479101186073129511813461860645042.FatRat;

say $ramanujan;

The value has 119 characters (18 digits before the decimal point, the decimal point itself, and finally 100 digits after the decimal point). This clearly satisfies the demand for «at least 32 digits of precision». That is digits after the decimal point.

I got this 118 digit number from the Perl 6 section of rosettacode.org/wiki/Ramanujan's_constant. And if you think that my solution is cheating, take a look at the (non-cheating) solutions presented by Rosetta Code.

Ramanujan, Third Try

I'm not ready to give up yet. I plugged in FatRat values for «pi» and «e» in the original equation:

File: ramanujan2 (abridged)
my FatRat $pi = 3.14159265358979323 ... 9216420198938095.FatRat;
my FatRat $e = 2.718281828459045235 ... 889570350354.FatRat;

say $e ** ($pi * sqrt(163));

I got the value of «e» from www.miniwebtool.com/first-n-digits-of-e

Running it gives exactly the same result as when we used the built in «pi» and «e», and the value is a «Num».

The problem is that the «sqrt» call gives us a «Num», and that leads to the result also being a «Num». Coercing it to «FatRat» doesn't help, as it is done after the fact. And besides the square root is horribly truncated.

Andrew Shitow's FatRat vs Rat in Perl 6 article presents «Newton’s method of finding an approximate value of a square root», and gives us a «FatRat value». I wrapped it up as a procedure:

File: ramanujan2 (changes only)
sub FatRatRoot (Int $int where $int > 0, :$precision = 10)
{
  my @x = 
    FatRat.new(1, 1), 
    -> $x { $x - ($x ** 2 - $int) / (2 * $x) } ... *;

  return @x[$precision];
}

say $e ** ($pi * FatRatRoot(163));

And guess what? This gives the same annoying «Num» result as well:

$ perl6 ramanujan2
2.625374126407677e+17

The final problem is the exponentiation operator, which returns a «Num», even if both operands are «FatRat»s. To quote the documentation for «**»: «If the right-hand side is a non-negative integer and the left-hand side is an arbitrary precision type (Int, FatRat), then the calculation is carried out without loss of precision.»

The right hand side is not an integer, so this doesn't apply. And the calculation is carried out with loss of precision.

So yes, I am ready to give up now.