with Raku

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

Write a script that computes the equal point in the Fahrenheit and Celsius scales, knowing
that the freezing point of water is 32 °F and 0 °C, and that the boiling point of water is
212 °F and 100 °C. This challenge was proposed by |

We start by finding the conversion formula, given the facts in the challenge:

- The numeric difference between 0 and 100 °C is 100
- The numeric difference between 32 and 212 °F is 180
- A 1 °C increase == a 1.8 °F increase
- 32 °F == 0 °C

This gives us the conversion formula:

F = 1.8 C + 32

1.8 is the same as 9/5, which you will see more often if you look up the formula.

As a procedure, it looks like this:

sub celcius2fahrenheit ($c)
{
return $c * 1.8 + 32;
}

We should test it with the values given in the challenge, to see that it is correct:

> sub celcius2fahrenheit ($c) { return $c * 1.8 + 32; }
> say celcius2fahrenheit(0); # -> 32
> say celcius2fahrenheit(100); # -> 212

We got it right.

As a graph it looks like this:

To convert from Celsius, look up the value in the C direction, follow the row to the right until you reach the blue line. Then go straight down, and read the Fahrenheit value. The other direction is just the other way round.

The **1.8** part means that the Fahrenheit value grows faster than the Celsius value,
and the numerical difference will increase. That means that they will meet somewhere
lower than 0 °C, as the numerical difference will decrease (until the scales cross each other,
and the difference increases again).

my $c = 0; # [1a]
loop # [1]
{
my $f = celcius2fahrenheit($c); # [2]
if $f <= $c # [3]
{
say "Fahrenheit ($f) and Celsius ($c) are equal(ish)."; # [4]
last; # [4]
}
$c--; # [1b]
}
sub celcius2fahrenheit ($c)
{
return $c * 1.8 + 32;
}

[1] An eternal loop, starting with zero [1a] and counting down 1 [1b] each time.

[2] Calculate the Fahrenheit equivalent of the Celsius input.

[3] As long as the Fahrenheit value is larger, continue with the loop.

[4] If the Fahrenheit value isn't larger anymore, print the values and exit.

The «`<=`

» instead of «`==`

» (in [3]) ensures that it works even
if the point where the two scales cross each other isn't an integer. The result
will not be accurate in that case, but it is almost correct. (Feel free to contemplate
the philosophical difference between «almost correct» and «not correct».)

Running it:

$ raku fahrenheit-celcius
Fahrenheit (-40) and Celsius (-40) are equal(ish).

We were lucky, and got the exact result as they intersect on a decimal value.

F = 1.8 C + 32 && F = C
F = 1.8 F + 32
0.8 F = -32
F = -40
F = C = -40

This approach gave the exact answer, and would have worked even if it hadn't been an integer. So in my view this isn't a programming excercise, but a mathematical one.

As a program, if you insist:

File: fahrenheit-celcius2say "Fahrenheit and Celsius are equal at -40.";

Yes, it is silly. But it uses an alorithm. A silly algorithm.

Write a script to create an Identity Matrix for the given size. For example, if the size is 4,
then create Identity Matrix 4x4. For more information about |

The module Math::Matrix can be
used to create a matrix, and it even supports Identity Matrices *out of the box*!

Install it with «zef install Math::Matrix» (or «sudo zef install Math::Matrix», depending on your setup).

File: identity-matrix-mmuse Math::Matrix; # [1]
unit sub MAIN (Int $size where $size > 0); # [2]
my $im = Math::Matrix.new-identity( $size ); # [3]
say $im; # [4]

[1] Load the module.

[2] Ensure we get a positive integer (from 1 and up) as argument.

[3] The module does the job for us.

[4] The challenge didn't specify *what to do with the Identity Matrix* once we have one,
but printing it shows that we got it right.

Running it:

$ raku identity-matrix-mm 1
1
$ raku identity-matrix-mm 2
1 0
0 1
$ raku identity-matrix-mm 3
1 0 0
0 1 0
0 0 1
$ raku identity-matrix-mm 4
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1

Using a module made it easy, and shows off the power of modules and reuse of code. Why reinvent the wheel?

Reinventing the wheel can be fun. So let us rewrite it without using a module. A matrix is an array with more than one dimension (in this case 2), and Raku's «Shaped Arrays» are ideal for that.

A shaped array is just an array with a shape, or fixed size:

> my @a[2]; # -> [(Any) (Any)]

Accessing an index outside of the given limit will cause an error. The shape is the number of items, but the indices still go from zero (0 and 1 in this case):

> @a[0] = 1; # -> 1
> @a[1] = 1; # -> 1
> @a[2] = 1; # -> Index 2 for dimension 1 out of range (must be 0..1)

A Raku array can have several dimensions:

> my @a[2;2]; # -> [[[(Any) (Any)] [(Any) (Any)]]
> my @a[4;2]; # -> [[(Any) (Any)] [(Any) (Any)] [(Any) (Any)] [(Any) (Any)]]

The «shape» method can be used to get the shape, if you need it:

> say @a.shape; # -> (4 2)

See docs.raku.org/language/list#Fixed_size_arrays for more information about Shaped Arrays.

A Shaped Array has no initial value(s), as indicated by the «Any» value. This is consistent for all types of variables, so we must add the initial zeros manually. And of course the ones.

File: identity-matrix-shapeduse Math::Matrix;
unit sub MAIN (Int $size where $size > 0);
my @m[$size;$size] = 0 xx $size xx $size; # [1]
@im[$_;$_] = 1 for ^$size; # [2]
say @im;

[1] We start with the value 0, and use the List Repetition Operator «xx» to give us a list of $size number of zeroes. Then we apply the «xx» operator again to give us $size number of that list. The result is a matrix where all the values are 0.

[2] The value 1 is inserted in the diagonal. The indices are the same for x and y, and go
from 0 to the shape - 1. `^$size`

gives us exactly that. (Think of «^» as
*upto (but not including)*.)

See docs.raku.org/routine/xx for more information about the List Repetition Operator «xx».

Running it:

$ raku identity-matrix-shaped 1
[[1]]
$ raku identity-matrix-shaped 2
[[1 0] [0 1]]
$ raku identity-matrix-shaped 3
[[1 0 0] [0 1 0] [0 0 1]]
$ raku identity-matrix-shaped 4
[[1 0 0 0] [0 1 0 0] [0 0 1 0] [0 0 0 1]]

The shaped arrays don't look as nice as the output provided by «Math::Matrix» when we print them, but we can fix that. Later.

If you find it inelegant first set all the values to zero, and then change the diagnal to one, do it once like this:

File: identity-matrix-shaped2unit sub MAIN (Int $size where $size > 0);
my @row = (1, 0 xx $size -1).flat; # [1]
my @x; @x.push: @row.rotate(- $_) for ^$size; # [2]
my @im[$size;$size] = @x; # [3]
say @im;

[1] The first row starts with 1, and the rest is zeroes. The number of zeroes is the size minus 1. The final «flat» ensures a one dimensional list. Forgetting it is not a good idea:

> (1, 0 xx 3).flat; # -> (1 0 0 0)
> (1, 0 xx 3); # -> (1 (0 0 0))

[2]
We push a copy of the first row, rotating it *X* positions
to the left for each line. *X* is the index of the line (0, 1, ...).

[3] It is illegal to «push» to a shaped array, so we have to use a temporary variable (@x), and assign it when it is complete.

See docs.raku.org/routine/rotate for more information about «rotate».

I hope you agree that the initial approach (assign all to zero, and then the diagional to 1) is better, as it is easier to understand. As to what is faster to execute, feel free to guess. We have fewer assignments in the rotate version, but rotations require some work as well.

unit sub MAIN (Int $size where $size > 0);
my @im[$size;$size] = 0 xx $size xx $size;
@im[$_;$_] = 1 for ^$size;
print @im.&nice-format; # [1]
sub nice-format (@shaped) # [2]
{
my ($row, $col) = @shaped.shape; # [3]
my $result; # [4]
for ^$row -> $x # [5]
{
for ^$col -> $y # [5]
{
$result ~= @shaped[$x;$y] ~ " "; # [6]
}
$result ~= "\n"; # [7]
}
return $result; # [8]
}

[1] Last week I showed that it is possible to call a procesdure like a method, and here we go again.

[2] The procedure.

[3] Get the shape. Allow different sizes for the rows and columns. Note that the procedure doesn't support more than two dimensions, and doesn't bother to check the input as it should.

[4] Bulid up the result, to be returned in [8].

[5] A two dimentional for loop.

[6] Add one element at a time.

[7] Add a newline after each row.

[8] Return the result.

Also note the heavy use of «$size» in the first three lines; 7 times. And then never mentioned again.

Running it:

$ raku identity-matrix-nice 5
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 1 0
0 0 0 0 1

Note that it isn't possible to get a whole row at a time; just a single cell (value) - or the whole array. This limitation applies to Shaped Arrays only:

> my @a[3;3] = [[1,1,1], [2,2,2], [3,3,3]];
> say @a[1];
Partially dimensioned views of shaped arrays not yet implemented. Sorry.
> my @a = [[1,1,1], [2,2,2], [3,3,3]];
> say @a[1]; # -> [2 2 2]
> say @a[4]; #> (Any)

Doing it with an unshaped array:

File: identity-matrix-nicerunit sub MAIN (Int $size where $size > 0);
# my @im = 0 xx $size xx $size; # [1]
# @im[$_;$_] = 1 for ^$size; # [1]
my @row = (1, 0 xx $size -1).flat; # [2]
my @x; @x.push: @row.rotate(- $_) for ^$size; # [2]
my @im = @x; # [2]
print @im.&nice-format;
sub nice-format (@array) # [3]
{
return (@($_).join(" ") for @array).join("\n") ~ "\n"; # [3]
}

[1] Assigning to a single element works for Shaped Arrays only, so these lines will casue a compile time error!

[2] So we have to stick to the way we did it in «identity-matrix-shaped2».

[3] And this procedure is much nicer now. It starts with the top level list of rows
(`for @array`

), takes the content (the actual row; `@($_)`

) and
glues it together with spaces between the values (`join(" ")`

). Then it
joins all the rows together with a newline between them (`join("\n")`

).
Finally it adds a trailing newline (` ~ "\n"`

) and returns the lot. All
in a single line of code...

Your idea of «nice» may differ. But it is much shorter.