I spent a few days without internet access, and so I took all the energy I normally spent surfing the internet and instead channeled it into Rosella. Because of the effort, I’ve been able to refactor an older library that I’ve been playing with and bring it up to proper, stable status. This new library, “Query” is a library for higher-order functions over aggregate objects. It’s been heavily influenced by the System.Linq library from the world of .NET. People who have .NET experience should be familar with all the concepts and techniques in the Query library.
To start working with the Query libray, we need to take an object we want to
play with and wrap it up in a Queryable
. For .NET fans out there, Queryable
is like a combination of IEnumerable
with the various Linq extension methods
on it already.
my @data := [1, 2, 3, 4];
my $q := Rosella::Query::as_queryable(@data);
Once we have the Queryable, we can start running methods on it. Most methods on Queryable return a new Queryable, so we can chain result sets. Here’s a quick example taken out of the test suite:
my @data := [1, 2, 3, 4, 5, 6, 7, 8, 9];
my $sum := Rosella::Query::as_queryable(@data)
.map(sub($i) { return $i * $i; })
.filter(sub($j) { return $j % 2; })
.fold(fsub($s, $i) { return $s + $i; })
.data();
pir::say($sum); # 165
One departure I have made from the Linq library is to use the more “classic”
names for the various operations. Methods .Select()
, ‘.Where(), and
.Aggregate() are called "map", "filter", and "fold" respectively. In this
example above we start with an array of integers from 1 to 9. We square each
element with
map, use
filter to remove the even elements, use
fold to
sum them all together, and finally we use the helper method
.data() to
get access to the internal result data of the Queryable. The final result is
1 + 9 + 25 + 49 + 81 = 165`, as we expect.
The Rosella::Query::Queryable
class is basically a facade
over the underlying mechanism Rosella::Query::Provider
and subclasses. The
Provider types provide the Query behaviors for different data types. Right now
I have one for Array, Hash, and Scalar. Others can be used instead, if you
want to provide a custom subclass. The necessary interface is a little bit
larger than I would like right now, but it’s not impossible to provide your
own custom type if you want to.
Other features that I’ve added are the abilities to convert data types to different output formats easily. Here’s an example where we convert an array to a hash:
my @data := [1, 2, 3, 4];
my %hash := Rosella::Query::as_queryable(@data)
.to_hash(sub($i) { return "Square $i"; })
.map(sub($i) { return $i * $i; })
.data;
The function reference to .to_hash()
takes each element from the array and
is expected to return a string key for inserting it into the hash. We start
out with an array [1, 2, 3, 4]
, and end up with a hash:
%hash = {
"Square 1" => 1,
"Square 2" => 4,
"Square 3" => 9,
"Square 4" => 16
}
Which is not too bad to do in one statement, albeit a complicated one. With this library, it’s pretty easy to break up complicated loops and nested loops and other operations into a logical sequence of small individual operations.
Map, Filter, and Fold are common operations that most programmers should be familiar with under one name or another. The to_array and to_hash convert between the two aggregate types. There are a number of other operations in the Query library as well: Count, Any, Single, First, First_or_Default, Take and Skip. There are a few other operations I might like to add as well, in future revisions, but this is a good start for now.
If you’re used to System.Linq, this should be very familiar and maybe even useful to you. I’d be lying if I said that this infrastructure didn’t come with some additional overhead compared to writing out the loops yourself, but the tradeoff in terms of simplicity and readability might be worth the extra runtime cost for some people. I don’t think it’s a huge amount of extra cost, but every little bit is something worth considering.
For more information about this library, see the online documentation for it.
There are three things that I can see wanting to do for this library in the future: Add more providers for complex and custom data types, add new query methods to the providers, and add a set of canned predicates for performing common operations, probably through a nice interface somewhere. These things will all be done eventually, and should be able to be implemented in a way which does not break backwards compatibility.