Perl6::Math::Matrix (Part 2: Converter)
In this series of articles I reflect and expand on a talk I gave this year in Glasgow were I spoke about writing Perl 6 modules in general and Math::Matrix in particular. Part I was about data types I used or wanted to use, because my approach is it to think first about data structures and built later the code around that. It is also crucial because this module basically provides the user a new data type.
To continue where we left off, this part will be about converting our data type (Math::Matrix) into other types. And as I said in the intro of my talk: "your main job as a Perl 6 module author is to not screw up the inherit beauty of the internal design" - meaning: you have to provide for a lot of interfaces - read: implement certain methods! Converter to popular data types are just one part of it.
It also makes your data type also much more usable if it provides meaningful information in often used contexts. A context is in Perl 6 also nothing else than a situation where a certain method is called upon you object. And most of the time this method will be a converter into a type as well.
You might not use the prefix operator ? or its word like counterpart "so" as often, but an "if" is the bedrock of programming (the ternary op or any other logical op do also enforce Bool context). And since this module is obviously about math and all Numeric types (if asked about their boolean value) just check if the value is not zero, we should do the same. It is handy, that there is a thing called "zero" matrix (when all cells have value of 0). So in bool context I just deferred to the method is-zero. This is another Perl 6-convention we should not break: methods that return a Bool start with an "is-".
This context, enforced by an infix or prefix plus or minus operator is more tricky. For the longest time it returned the count of elements. But this mad not that much sense. First of all a 2 * 2 and a 4 * 1 matrix are very different things so +$matrix1 == +?matrix2 would be rather misleading, returning the two dimensional size would make much more sense, but the method Numeric expects one value. The second issue: it is a mismatch with what I wrote in part one ("I see the matrix as one singular value"), .. and not a container I might add. Plus there there is already a mathematical term for a singular numerical value representing the "size" of a matrix content: norm. That is why it redirects now to the norm method, which gives you the Euclidean norm (sometimes called "Frobenius" or "L2"), when called without arguments. An alternative candidate would be the determinant of the matrix, but not every matrix has one (only square matrices), so its better to take the norm here.
This one is again easy, since everyone knows what to expect (I got it wrong the first time anyway - but the audience luckily caught me out). The rows are separated by new lines and the cells by spaces is how we write down a matrix on paper. This allows for nicely formatted matrices within the source code by using heredocs.
Math::Matrix.new: q:to/END/; # indent as you wish 1 2 3 4 END
Conveniently new also accepts this format as input, so we are also consistent there.
The main format new expects (and as firstly implemented by Pierre Vigier) is an Array of Arrays. So we should have also a method that can return this format.
Plus I implemented the AT-POS method, which allows this logical and very natural syntax to access a cell $matrix (readonly). The method itself returns of course just the array with the values of a row. This is just another builtin mechanism of the language we can easily use by writing a one line method.
This is more of a toy, but when we have the content as an Array, why not have an Hash and work with these subscripts. Since Perl is much about data munging some might even need it to work efficiently on their data, because it is important to me to not make the format opaque. You might just use one or two math operations to transform your data and than reuse it elsewhere.
Returning a flat list (which is the expected) is no biggie, but there are several issues attached to it. You might want a row wise or column wise ordering, or even a list of lists. To fulfill all these needs I also implemented list-rows and list-column, which fulfills the last wish. the method list is just an alias to list-rows.flat, so if that is not what you want, you can at least call list-columns.flat.
Because lists are so useful, this method opens up the module to several worlds of functionalities. One of these are set operators. Does our matrix contain the value 1 ($matrix (cont) 1). To ease this syntax, there is also a cont - method: $matrix.cont(1). To ask if there are values between one and three in the matrix you could ask: $matrix.cont(1..3). If you want to know if all values of the matrix are in a certain range (are elements of a range), write: $matrix.elem(2..7).
And surely your can map, grep, reduce and alike on the lists you get out the matrix content. As a shortcut - the module provides its own map, which maps over all cells and creates a new matrix out of the results (following the logic that normal map takes a list and transforms it to another list of results). You could also reduce the rows (reduce-rows) or (reduce-columns) to create a list out of the whole matrix. For instance $matrix.reduce-columns(&[+]) gives you a list in which every value is the sum of the values in a matrix column.
The method .Str is also called by print and put. And frankly it is embarrassing when your user does print $var-with-your-datatype and it returns nothing or something that is not representative of the content of your object. Even more crucial, because more often used is say, which not only attaches a new line - it will call the method gist on your object. The name gist tells is all, for instance a list will be shortened to the first 100 elements (to not spam the shell) when you say $list. Accordingly say $matrix will show you a nicely formatted excerpt that fits on a standard terminal. But optional parameters allow you to adjust to any screen size. I'm also glad to announce the main feature of the newly released version 0.3.0 that gist now works with all Numeric types (including Bool and Complex) and it will be formatted in a way so that each column takes minimal space and complex numbers will stick out. I plan to provide third optional parameter for other formatting rules.
The output of gist will be cached, so after once you set your format, you can later just say $matrix instead of say $matrix.gist(...). (Don't worry you can change the format later by calling gist with different arguments).
And if you want to be very correct: provide 2 .gist multi methods. One for the defined object and one for the undefined value (type object), because any standard type object is also known by gist. (say Int gives you "(Int)").
Even if I provide both data formats accepted by new, I should also have a perl method, because that is expected from me, the module author even more: to have a method which output can be eval-ed into a clone (for marshaling and other purposes).