Exegesis 4
by Damian Conway
|
Pages: 1, 2, 3, 4, 5, 6
Editor's note: this document is out of date and remains here for historic interest. See Synopsis 4 for the current design information.
Onward and Backward
Back in our example, after each token has been dealt with in its loop iteration, the iteration is finished. All that remains to do is increment the token number.
In Perl 5, that would be done in a continue block at the end of the loop block. In Perl 6, it's done in a NEXT statement within the loop block:
NEXT { $toknum++ }
Like a CATCH, a NEXT is a special-purpose BEGIN block that takes a closure as its single argument. The NEXT pushes that closure onto the end of a queue of "next-iteration" handlers, all of which are executed each time a loop reaches the end of an iteration. That is, when the loop reaches the end of its block or when it executes an explicit next or last.
The advantage of moving from Perl 5's external continue to Perl 6's internal NEXT is that it gives the "next-iteration" handler access to any lexical variables declared within the loop block. In addition, it allows the "next-iteration" handler to be placed anywhere in the loop that's convenient (e.g. close to the initialization it's later supposed to clean up).
For example, instead of having to write:
# Perl 5 code
my $in_file, $out_file;
while (<>) {
open $in_file, $_ or die;
open $out_file, "> $_.out" or die;
# process files here (maybe next'ing out early)
}
continue {
close $in_file or die;
close $out_file or die;
}
we can just write:
while (<>) {
my $in_file = open $_ or die;
my $out_file = open "> $_.out" or die;
NEXT {
close $in_file or die;
close $out_file or die;
}
# process files here (maybe next'ing out early)
}
There's no need to declare $in_file and $out_file outside the loop,
because they don't have to be accessible outside the loop (i.e. in an external continue).
This ability to declare, access and clean up lexicals within a given scope is especially important because, in Perl 6, there is no reference counting to ensure that the filehandles close themselves automatically immediately at the end of the block. Perl 6's full incremental garbage collector does guarantee to eventually call the filehandle's destructors, but makes no promises about when that will happen.
Note that there is also a LAST statement, which sets up a handler that is called automatically when a block is left for the last time. For example, this:
for reverse 1..10 {
print "$_..." and flush;
NEXT { sleep 1 }
LAST { ignition() && print "lift-off!\n" }
}
prints:
10...9...8...7...6...5...4...3...2...1...lift-off!
sleeping one second after each iteration (including the last one), and then calling &ignition at the end of the countdown.
LAST statements are also extremely useful in nonlooping blocks, as a way of giving the block a "destructor" with which it can clean up its state regardless of how it is exited:
sub handler ($value, $was_handled is rw) {
given $value {
LAST { $was_handled = 1 }
when &odd { return "$value is odd" }
when /0$/ { print "decimal compatible" }
when /2$/ { print "binary compatible"; break }
$value %= 7;
when 1,3,5 { die "odd residual" }
}
}
In the above example, no matter how the given block exits -- i.e. via the return of the first when block, or via the (implicit) break of the second when, or via the (explicit and redundant) break of the third when, or via the "odd residual" exception, or by falling off the end of the given block -- the $was_handled parameter is always correctly set.
Note that the LAST is essential here. It wouldn't suffice to write:
sub handler ($value, $was_handled is rw) {
given $value {
when &odd { return '$value is odd" }
when /3$/ { print "ternary compatible" }
when /2$/ { print "binary compatible"; break }
$value %= 7;
when 1,3,5 { die "odd residual" }
}
$was_handled = 1;
}
because then $handled wouldn't be set if an exception was thrown. Of course, if that's actually the semantics you wanted, then you don't want LAST in that case.
WHY ARE YOU SHOUTING???
You may be wondering why try is in lower case but CATCH is in upper.
Or why NEXT and LAST blocks have those "loud" keywords.
The reason is simple: CATCH, NEXT and LAST blocks are just specialized BEGIN blocks that install particular types of handlers into the block in which they appear.
They install those handlers at compile-time so, unlike a try or a next or a last, they don't actually do anything when the run-time flow of execution reaches them. The blocks associated with them are only executed if the appropriate condition or exception is encountered within their scope. And, if that happens, then they are executed automatically, just like AUTOLOAD, or DESTROY, or TIEHASH, or FETCH, etc.
So Perl 6 is merely continuing the long Perl tradition of using a capitalized keyword to highlight code that is executed automatically.
In other words: I'M SHOUTING BECAUSE I WANT YOU TO BE AWARE THAT SOMETHING SUBTLE IS HAPPENING AT THIS POINT.
Cache and Return
Meanwhile, back in calc...
Once the loop is complete and all the tokens have been processed, the result of the calculation should be the top item on the stack. If the stack of items has more than one element left, then it's likely that the expression was wrong somehow (most probably, because there were too many original operands). So we report that:
fail Err::BadData : msg=>"Too many operands"
if @stack > 1;
If everything is OK, then we simply pop the one remaining value off the stack and make sure it will evaluate true (even if its value is zero or undef) by setting its true property. This avoids the potential bug discussed earlier.
Finally, we record it in %var under the key '$n' (i.e. as the n-th result), and return it:
return %var{'$' _ $i} = pop(@stack) but true;
"But, but, but...", I hear you expostulate, "...shouldn't that be pop(@stack) is true???"
Once upon a time, yes. But Larry has recently decided that compile-time and run-time properties should have different keywords. Compile-time properties (i.e. those ascribed to declarations) will still be specified with the is keyword:
class Child is interface;
my $heart is constant = "true";
our $meeting is private;
whereas run-time properties (i.e. those ascribed to values) will now be specified with the but keyword:
$str = <$trusted_fh> but tainted(0);
$fh = open($filename) but chomped;
return 0 but true;
The choice of but is meant to convey the fact that run-time properties will generally contradict some standard property of a value, such as its normal truth, chompedness or tainting.
It's also meant to keep people from writing the very natural, but very misguided:
if ($x is true) {...}
which now generates a (compile-time) error:
Can't ascribe a compile-time property to the run-time value of $x.
(Did you mean "$x but true" or "$x =~ true"?)
The Forever Loop
Once the Calc module has all its functionality defined, all that's required is to write the main input-process-output loop. We'll cheat a little and write it as an infinite loop, and then (in solemn Unix tradition) we'll require an EOF signal to exit.
The infinite loop needs to keep track of its iteration count. In Perl 5 that would be:
# Perl 5 code
for (my $i=0; 1; $i++) {
which would translate into Perl 6 as:
loop (my $i=0; 1; $i++) {
since Perl 5's C-like for loop has been renamed loop in Perl 6 -- to distinguish it from the Perl-like for loop.
However, Perl 6 also allows us to create semi-infinite, lazily evaluated lists, so we can write the same loop much more cleanly as:
for 0..Inf -> $i {
When Inf is used as the right-hand operand to .., it signifies that the resulting list must be lazily built, and endlessly iterable. This type of loop will probably be common in Perl 6 as an easy way of providing a loop counter.
If we need to iterate some list of values, as well as tracking a loop counter, then we can take advantage of another new feature of Perl 6: iteration streams.
A regular Perl 6 for loop iterates a single stream of values, aliasing the current topic to each in turn:
for @stream -> $topic_from_stream {
...
}
But it's also possible to specify two (or more) streams of values that the one for loop will step through in parallel:
for @stream1 ; @stream2 -> $topic_from_stream1 ; $topic_from_stream2 {
...
}
Each stream of values is separated by a semicolon, and each topic variable is similarly separated. The for loop iterates both streams in parallel, aliasing the next element of the first stream (@stream1) to the first topic ($topic_from_stream1) and the next element of the second stream (@stream2) to the second topic ($topic_from_stream2).
The commonest application of this will probably be to iterate a list and simultaneously provide an iteration counter:
for @list; 0..@list.last -> $next; $index {
print "Element $index is $next\n";
}
It may be useful to set that out slightly differently, to show the parallel nature of the iteration:
for @list ; 0..@list.last
-> $next ; $index {
print "Element $index is $next\n";
}
It's important to note that writing:
for @a; @b -> $x; $y {...}
# in parallel, iterate @a one-at-a-time as $x, and @b one-at-a-time as $y
is not the same as writing:
for @a, @b -> $x, $y {...}
# sequentially iterate @a then @b, two-at-a-time as $x and $y
The difference is that semicolons separate streams, while commas separate elements within a single stream.
If we were brave enough, then we could even combine the two:
for @a1, @a2; @b -> $x; $y1, $y2 {...}
# sequentially iterate @a1 then @a2, one-at-a-time as $x
# and, in parallel, iterate @b two-at-a-time as $y1 and $y2
This is definitely a case where a different layout would help make the various iterations and topic bindings clearer:
for @a1, @a2 ; @b
-> $x ; $y1, $y2 {...}
Note, however, that the normal way in Perl 6 to step through an array's values while tracking its indices will almost certainly be to use the array's kv method. That method returns a list of interleaved indices and values (much like the hash's kv method returns alternating keys and values):
for @list.kv -> $index, $next {
print "Element $index is $next\n";
}

