A Matrix Worthy of Perl 6
The Problem
Wouldn't it be nice to have symbolic matrix operations in Perl? Instead of composing matrices of numbers, we could compose matrices of expressions. Consider the following:
use strict;
use warnings;
use Math::MatrixReal;
my $x = 1;
my $m = Math::MatrixReal->new_from_rows([ [$x, 2*$x], [3*$x, 4*$x] ]);
for(; $x < 4; ++$x)
{
print "$m\n";
}
The output of this code is:
[ 1.000000000000E+00 2.000000000000E+00 ] [ 3.000000000000E+00 4.000000000000E+00 ] [ 1.000000000000E+00 2.000000000000E+00 ] [ 3.000000000000E+00 4.000000000000E+00 ] [ 1.000000000000E+00 2.000000000000E+00 ] [ 3.000000000000E+00 4.000000000000E+00 ]
This isn't surprising. After all, we're just passing a scalar value for the matrix elements. If we wanted the expression to be re-evaluated each time, we'd need to enclose it in a coderef. However, this won't solve the problem because Math::MatrixReal wants numbers, not coderefs.
By now, you've decided to re-implement Math::MatrixReal. However, if you define a matrix with coderef elements, what happens when you multiply it with another matrix?
my $m1 = Math::MatrixReal->new_from_rows([ [sub {$x}, sub {2*$x}], [sub {3*$x}, sub {4*$x}] ]);
my $m2 = Math::MatrixReal->new_from_rows([ [sub {10*$x}, sub {20*$x}], [sub {30*$x}, sub {40*$x}] ]);
my $product = $m1->multiply($m2);
There are three options for how this would work: (The examples are dumbed down because matrix multiplication is rather long.)
Each element is evaluated for the multiplication:
my $a = sub {2}; my $b = sub {3}; my $result = $a->() * $b->();
This sort of defeats the purpose of using coderefs, since the expressions won't carry all of the way through.
Multiply the coderefs:
my $a = sub {2}; my $b = sub {3}; my $result = $a * $b;
This "works", in the sense that it doesn't blow up. However, $a and $b are essentially pointers to functions. Doing multiplication on them makes Perl treat them as actual numbers, and the $result has no physical or logical meaning.
- Use a tree to keep track of the operations within the expression, and then traverse the tree to evaluate the expression when printing.
This will work! However, it wouldn't be very much fun to implement or maintain.
So we have a workable solution, but it's not fun, and it's not really elegant. You'd have to mix tree operations in with your matrix math code. I've already lost interest.
Enter Perl 6
In Perl 6, everything is an object. Yes, even coderefs. (Now they're called Blocks.) What does this mean for us? First of all, we can no longer multiply Blocks:
my $code = {2};
say $code*$code;
Result:
Method 'Num' not found for invocant of class 'Block' in 'Cool::Numeric' at line 1739:CORE.setting in 'Cool::Numeric' at line 1740:CORE.setting in 'infix:<*>' at line 6460:CORE.setting in main program body at line 14:./perl6-test.pl
This error reveals a key piece of information: Num() is not defined for Block, so it doesn't make sense to try to multiply them.
Monkey Typing
Perl 6 supports "monkey typing", or augmenting a class with additional functionality:
use MONKEY_TYPING;
augment class Block
{
method Num
{
return self.();
}
}
my $code = {2};
say $code*$code;
Result:
4
See how MONKEY_TYPING is in all caps? That's (I'm assuming) to indicate to you that you're doing something weird and unusual. Monkey typing has a global effect. Using this approach, every Block will get the Num() method. This isn't necessarily a bad thing, but it will make debugging an accidental Block in a numeric context much harder. It's best to avoid monkey typing, especially when interacting with other people's code.
Roles
If you're familiar with Perl 6's object model, or Perl 5's Moose, you'll remember that you can assign roles to individual objects. What if we made a role with a Num() method?
role BlockNumify
{
method Num
{
return self.();
}
};
my $code = {2};
$code does BlockNumify;
say "I expect this to work: ", $code*$code;
my $code2 = {3};
say "I expect this to fail: ", $code2*$code2;
Result:
I expect this to work: 4 Method 'Num' not found for invocant of class 'Block' in 'Cool::Numeric' at line 1739:CORE.setting in 'Cool::Numeric' at line 1740:CORE.setting in 'infix:<*>' at line 6460:CORE.setting in main program body at line 16:./perl6-test.pl
This allows us to call Num() on Blocks that we are expecting to be treated as numbers, while also preserving the usual type safety with Blocks that we don't touch.
Putting it all Together
I'm not going to implement an entire Matrix class here because it would be long and tedious. I do have an example that should illustrate the point of all of this though:
my @operations;
my $x = 0.3;
my $code = {sin($x)};
$code does BlockNumify;
for 0..10 -> $i
{
@operations[$i] = {$i + $code};
@operations[$i] does BlockNumify;
}
my $sum = { [+] @operations };
$sum does BlockNumify;
for 1..5 -> $i
{
$x *= $i; # We modify $x all the way down here!
my $average = $sum/@operations.elems;
say $average;
}
Result:
5.29552020666134 5.56464247339504 5.9738476308782 5.79366786384915 4.00822114655688
The function of this code doesn't matter, but it does show off some important things. Perhaps most important is that $x is set at the beginning, and used in $code. Stuff happens, and at the end, we're looping around some code that doesn't modify $code anymore, but does modify $x, and results in different output. This shows that it works as intended. Hurray!
Why did I do the sum in such a strange way? The intuitive way would be to do something like:
my $sum = $code;
for 0..10 -> $i
{
$sum = {$i + $sum};
$sum does BlockNumify;
}
However, this causes infinite recursion, for reasons I don't fully understand. Thanks to the new reduction operators, it's easy enough to get around this.
A Quick Experiment
To show that this does behave as I've said it does, let's also output some debugging information:
my @operations;
my $x = 0.3;
my $code = {say "--> sin($x)"; sin($x)};
$code does BlockNumify;
for 0..2 -> $i
{
@operations[$i] = {say "-> $i + $code"; $i + $code};
@operations[$i] does BlockNumify;
}
my $sum = { say "Summing"; [+] @operations };
$sum does BlockNumify;
for 1..2 -> $i
{
$x *= $i;
my $average = $sum/@operations.elems;
say $average;
}
Result:
Summing -> 0 + _block141 --> sin(0.3) -> 1 + _block141 --> sin(0.3) -> 2 + _block141 --> sin(0.3) 1.29552020666134 Summing -> 0 + _block141 --> sin(0.6) -> 1 + _block141 --> sin(0.6) -> 2 + _block141 --> sin(0.6) 1.56464247339504
If we look at this as a tree, we can see that we do a depth-first traversal. Each new unknown value is immediately evaluated for the parent operation until we reach an actual number. This is exactly what we wanted at the beginning, but without explicitly messing around with trees.
Thanks for this post, I was looking for an explanation of MONKEY_TYPING and enjoy the example of a safer way around it for a particular purpose.