Sign In/My Account | View Cart  
advertisement


Listen Print

Exegesis 6
by Damian Conway | Pages: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11

Editor's note: this document is out of date and remains here for historic interest. See Synopsis 6 for the current design information.

Who Shall Sit in Judgment?

Conjuring up an anonymous subroutine in each call to part is intrinsically neither good nor bad, but it sure is ugly:

($cats, $chattels) = part sub($animal) { $animal.isa(Animal::Cat) }, @animals;

Fortunately, there's a cleaner way to specify the selector within the call to part. We can use a parameterized block instead:

($cats, $chattels) = part -> $animal { $animal.isa(Animal::Cat) } @animals;

A parameterized block is just a normal brace-delimited block, except that you're allowed to put a list of parameters out in front of it, preceded by an arrow (->). So the actual parameterized block in the above example is:

-> $animal { $animal.isa(Animal::Cat) }

In Perl 6, a block is a subspecies of Code object, so it's perfectly okay to pass a parameterized block as the first argument to &part. Like a real subroutine, a parameterized block can be subsequently invoked and passed an argument list. The body of the &part subroutine will continue to work just fine.

It's important to realize that parameterized blocks aren't subroutines though. They're blocks, and so there are important differences in their behaviour. The most important difference is that you can't return from a parameterized block, the way you can from a subroutine. For example, this:

part sub($animal) { return $animal.size < $breadbox }, @creatures

works fine, returning the result of each size comparison every time the anonymous subroutine is called within &part.

But in this "pointier" version:

part -> $animal { return $animal.size < $breadbox } @creatures

the return isn't inside a nested subroutine; it's inside a block. The first time the parameterized block is executed within &part it causes the subroutine in which the block was defined (i.e. the subroutine that's calling part) to return!

Oops.

The problem with that second example, of course, is not that we were too Lazy to write the full anonymous subroutine. The problem is that we weren't Lazy enough: we forgot to leave out the return. Just like a Perl 5 do or eval block, a Perl 6 parameterized block evaluates to the value of the last statement executed within it. We only needed to say:

part -> $animal { $animal.size < $breadbox } @creatures

Note too that, because the parameterized block is a block, we don't need to put a comma after it to separate it from the second argument. In fact, anywhere a block is used as an argument to a subroutine, any comma before or after the block is optional.

Cowabunga!

Even with the slight abbreviation provided by using a parameterized block instead of an anonymous subroutine, it's all too easy to lose track of the the actual data (i.e. @animals) when it's buried at the end of that long selector definition.

We can help it stand out a little better by using a new feature of Perl 6: the "pipeline" operator:

($cats, $chattels) = part sub($animal) { $animal.isa(Animal::Cat) } <== @animals;

The <== operator takes a subroutine call as its lefthand argument and a list of data as its righthand arguments. The subroutine being called on the left must have a slurpy array parameter (e.g. *@data) and the list on the operator's right is then bound to that parameter.

In other words, a <== in a subroutine call marks the end of the specific arguments and the start of the slurped data.

Pipelines are more interesting when there are several stages to the process, as in this Perl 6 version of the Schwartzian transform:

@shortest_first = map  { .key }                     # 4
              <== sort { $^a.value <=> $^b.value }  # 3
              <== map  { $_ => .height }            # 2
              <== @animals;                         # 1

This example takes the array @animals, flattens it into a list (#1), pipes that list in as the data for a map operation (#2), takes the resulting list of object/height pairs and pipes that in to the sort (#3), then takes the resulting sorted list of pairs and maps out just the sorted objects (#4).

Of course, since the data lists for all of these functions always come at the end of the call anyway, we could have just written that as:

@shortest_first = map  { .key }                     # 4
                  sort { $^a.value <=> $^b.value }  # 3
                  map  { $_ => .height }            # 2
                  @animals;                         # 1

But there's no reason to stint ourselves: the pipelines cost nothing in performance, and often make the flow of data much clearer.

One problem that many people have with pipelined list processing techniques like the Schwartzian Transform is that the pipeline flows the "wrong" way: the code reads left-to-right/top-to-bottom but the data (and execution) runs right-to-left/bottom-to-top. Happily, Perl 6 has a solution for that too. It provides a "reversed" version of the pipeline operator, to make it easy to create left-to-right pipelines:

@animals ==> map  { $_ => .height }              # 1
         ==> sort { $^a.value <=> $^b.value }    # 2
         ==> map  { .key }                       # 3
         ==> @shortest_first;                    # 4

This version works exactly the same as the previous right-to-left/bottom-to-top examples, except that now the various components of the pipeline are written and performed in the "natural" order.

The ==> operator is the mirror-image of <==, both visually and in its behaviour. That is, it takes a subroutine call as its righthand argument and a list of data on its left, and binds the lefthand list to the slurpy array parameter of the subroutine being called on the right.

Note that this last example makes use of a special dispensation given to both pipeline operators. The argument on the "sharp" side is supposed to be a subroutine call. However, if it is a variable, or a list of variables, then the pipeline operator simply assigns the list from its "blunt" side to variable (or list) on its "sharp" side.

Hence, if we preferred to partition our animals left-to-right, we could write:

@animals ==> part sub ($animal) { $animal.isa(Animal::Cat) } ==> ($cats, $chattels);

The Incredible Shrinking Selector

Of course, even with a parameterized block instead of an anonymous subroutine, the definition of the selector argument is still klunky:

($cats, $chattels) = part -> $animal { $animal.isa(Animal::Cat) } @animals;

But it doesn't have to be so intrusive. There's another way to create a parameterized block. Instead of explicitly enumerating the parameters after a ->, we could use placeholder variables instead.

As explained in Apocalypse 4, a placeholder variable is one whose sigil is immediately followed by a caret (^). Any block containing one or more placeholder variables is automatically a parameterized block, without the need for an explicit -> or parameter list. Instead, the block's parameter list is determined automatically from the set of placeholder variables enclosed by the block's braces.

We could simplify our partitioning to:

($cats, $chattels) = part { $^animal.isa(Animal::Cat) }
@animals;

Here $^animal is a placeholder, so the block immediately surrounding it becomes a parameterized block — in this case with exactly one parameter.

Better still, any block containing a $_ is also a parameterized block — with a single parameter named $_. We could dispense with the explicit placeholder and just write our partitioning statement:

($cats, $chattels) = part { $_.isa(Animal::Cat) }
@animals;

which is really a shorthand for the parameterized block:

($cats, $chattels) = part -> $_ { $_.isa(Animal::Cat) }
@animals;

Come to think of it, since we now have the unary dot operator (which calls a method using $_ as the invocant), we don't even need the explicit $_:

($cats, $chattels) = part { .isa(Animal::Cat) }
@animals;

Part: The Second

But wait, there's even...err...less!

We could very easily extend &part so that we don't even need the block in that case; so that we could just pass the raw class in as the first parameter:

($cats, $chattels) = part Animal::Cat, @animals;

To do that, the type of the first parameter will have to become Class, which is the (meta-)type of all classes. However, if we changed &part's parameter list in that way:

sub part (Class $is_sheep, *@data) {...}

then all our existing code that currently passes Code objects as &part's first argument will break.

Somehow we need to be able to pass either a Code object or a Class as &part's first argument. To accomplish that, we need to take a short detour into...

Pages: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11

Next Pagearrow