Day 6 – The X and Z metaoperators

One of the new ideas in Perl 6 is the metaoperator, an operator which combines with an ordinary operator in order to change its behaviour. There are several of them, but in this post we will concentrate on just two of them: X and Z.

The X operator is one you might already have seen in its ordinary role as the infix cross operator. It combines lists together, one element from each, in every combination:

> say ((1, 2) X ('a', 'b')).perl
((1, "a"), (1, "b"), (2, "a"), (2, "b"))

However, this infix:<X> operator is actually just a shorthand for the X metaoperator applied to the list concatenation operator infix:<,>. Indeed, you’re perfectly at liberty to write

> say ((1, 2) X, (10, 11)).perl
((1, 10), (1, 11), (2, 10), (2, 11))

If you like. So what happens if you apply X to a different infix operator? How about infix:<+>

> say ((1, 2) X+ (10, 11)).perl
(11, 12, 12, 13)

What’s it done? Instead of creating a list of all the elements picked for each combination, the operator applies the addition infix to them, and the result is not a list but a single number, the sum of all the elements in that combination.

This works for any infix operator you care to use. How about string concatenation, infix:<~>?

> say ((1, 2) X~ (10, 11)).perl
("110", "111", "210", "211")

Or perhaps the numeric equality operator infix:<==>?

> say ((1, 2) X== (1, 1)).perl
(Bool::True, Bool::True, Bool::False, Bool::False)

But this post is also meant to be about the Z metaoperator. We expect you may have already figured out what it does, if you’ve encountered the infix:<Z> operator before, which is of course just a shortcut for Z,. If a Haskell programmer understands infix:<Z> as being like the zip function, then the Z metaoperator is like zipWith.

> say ((1, 2) Z, (3, 4)).perl
((1, 3), (2, 4))
> say ((1, 2) Z+ (3, 4)).perl
(4, 6)
> say ((1, 2) Z== (1, 1)).perl
(Bool::True, Bool::False)

Z, then, operates on each element of each list in turn, working on the first elements together, then the second, then the third for however many there are. It stops when it reaches the end of a list regardless of which side that list is on.

Z is also lazy, so you can apply it to two infinite lists and it will only generate as many results as you need. X can only handle an infinite list on the left, otherwise it would never manage to get anywhere at all.

At the time of writing, Rakudo appears to suffer from a bug where infix:<Z> and infix:<Z,> are not identical: the former produces a flattened result list. S03 shows that the behaviour of the latter is correct.

These metaoperators, then, become powerful tools for performing operations encompassing the individual elements of multiple lists, whether those elements are associated in some way based on their indexes as with Z, or whether you just want to examine all possible combinations with X.

Got a list of keys and values and you want to make a hash? Easy!

my %hash = @keys Z=> @values;

Or perhaps you want to iterate over two lists in parallel?

for @a Z @b -> $a, $b { ... }

Or three?

for @a Z @b Z @c -> $a, $b, $c { ... }

Or maybe you want to find out all the possible totals you could get from rolling three ten-sided dice:

my @d10 = 1 ... 10;
my @scores = (@d10 X+ @d10) X+ @d10;

If you want to see some real-world use of these metaoperators, Sudoku.pm in Moritz Lenz’s Sudoku solver.

11 thoughts on “Day 6 – The X and Z metaoperators

  1. > X can only handle an infinite list on the left, otherwise it would never manage to get anywhere at all.

    This restriction only applies because S03 demands ordered results (“The returned lists are ordered such that the rightmost elements vary most rapidly.”) If you drop this requirement, X could easily take infinite lists on both sides (cross-products of countable sets are still countable)

  2. Is the Zip operator always equivalent to the corresponding hyper-operator?

    > (1,2) Z (3,4)
    1 3 2 4
    > (1,2) >>,< (1,2) Z+ (3,4)
    4 6
    > (1,2) >>+<< (3,4)
    4 6

    1. No, it’s only “equivalent” in the simple cases. The big difference is that zip is lazy and sequential, while hyper is eager and may execute in any order.

    2. Some additional differences are that hypers can work dwimmily on hierarchical values, while X and Z cannot, because they’re more list oriented. Also, since X and Z are looser than comma, you don’t have to parenthesize comma lists as you must with (most) hyper infixes. That is, hypers are transparent to the precedence of their base operator, while X and Z (and metasequences based on them) are forced to list infix precedence.

  3. Can anyone explain why the “.perl” is needed at the end of an expression for the code which begins with “say”? Thank you in advance.

    1. I certainly can. It’s just about output formatting for the sake of these examples. The .perl method in Perl 6 returns a string representation of the object which could be fed back through the compiler to reproduce the same object (in theory – it doesn’t work for absolutely everything. It’s a bit like Data::Dumper, if you know that from Perl 5). So I used .perl when I was writing the post in order to get the output in a form which shows the structure of the produced lists. If I’d omitted it, I’d have got the standard results from converting a list to a string, which would be something more like:

      > say (1, 2 X 3, 4)
      13142324

      Far less useful for the illustrative examples in the post.

  4. $ perl6 -v

    This is Rakudo Perl 6, version 2010.11-15-gfedc117 built on parrot 2.10.1 RELEASE_2_10_1-679-g9bec614

    $ perl6
    > ((1, 2) Z, (3, 4)).perl
    ((1, 3), (2, 4))
    > (1, 2) Z, (3, 4)
    2 4 2 4

    Why are the outputs different?

    Thanks.

    1. Because (1, 2) Z, (3, 4) is the equivalent of ((1, 2) Z, (3, 4)).Str, which is doing something along the lines of “Take each element in the list, convert it to a string, and return them separated by spaces.” .perl is more akin to Data::Dumper.

    2. As for why it says 2 4 2 4 rather than 1 3 2 4, that’s just a rakudobug that appears to involve accidental aliasing of returned variables.

  5. I think that the correct syntax is: -> ($a, $b) instead of -> $a, $b in your example (“iterate over two lists in parallel”), since the Z will result in a (list):

    instead of:
    for @a Z @b -> $a, $b { … }
    should be:
    for @a Z @b -> ($a, $b) { … }

    without parentheses I was getting the error “Too few positionals passed; expected 2 arguments but got 1”.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.