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.
A Trying Situation
Once the calculator's input has been split into tokens, the for loop processes each one in turn, by applying them (if they represent an operator), or jumping out of the loop (if they represent an end-of-expression marker: '.', ';', or '='), or pushing them onto the stack (since anything else must be an operand):
try {
when %operator { # apply operator
my @args = splice @stack, -2;
push @stack, %operator{$token}(*@args);
}
when '.', ';', '=' { # or jump out of loop
last;
}
use fatal;
push @stack, get_data($token); # or push operand
The first two possibilities are tested for using when statements. Recall that a when tests its first argument against the current topic. In this case, however, the token was made the topic by the surrounding for. This is a significant feature of Perl 6: when blocks can implement a switch statement anywhere there is a valid topic, not just inside a given.
The block associated with when %operator will be selected if %operator{$token} is true (i.e. if there is an operator implementation in %operator corresponding to the current topic). In that case, the top two arguments are spliced from the stack and passed to the closure implementing that operation (%operator{$token}(*@args)). Note that there would normally be a dot (.) operator between the hash entry (i.e. a subroutine reference) and the subroutine call, like so:
%operator{$token}.(*@args)
but in Perl 6 it may be omitted since it can be inferred (just as an inferrable -> can be omitted in Perl 5).
Note too that we used the flattening operator (C<*>) on C<@args>,
because the closure returned by C<%operator{$token}> expects two
scalar arguments, not one array.
The second when simply exits the loop if it finds an "end-of-expression" token. In this example, the argument of the when is a list of strings, so the when succeeds if any of them matches the token.
Of course, since the entire body of the when block is a single statement, we could also have written the when as a statement modifier:
last when '.', ';', '=';
The fact that when has a postfix version like this should come as no surprise, since when is simply another control structure like if, for, while, etc.
The postfix version of when does have one interesting feature. Since it governs a statement, rather than a block, it does not provide the block-when's automatic "break to the end of my topicalizing block" behavior. In this instance, it makes no difference since the last would do that anyway.
The final alternative -- pushing the token onto the stack -- is simply a regular Perl push command. The only interesting feature is that it calls the get_data subroutine to pre-translate the token if necessary. It also specifies a use fatal so that get_data will fail by an throwing exception, rather than returning undef.
The loop tries each of these possibilities in turn. And "tries" is the operative word here, because either the application of operations or the pushing of data onto the stack may fail, resulting in an exception. To prevent that exception from propagating all the way back to the main program and terminating it, the various alternatives are placed in a try block.
A try block is the Perl 6 successor to Perl 5's eval block.
Unless it includes some explicit error handling code (see Where's the catch???), it acts exactly like a Perl 5 eval {...}, intercepting a propagating exception and converting it to an undef return value:
try { $quotient = $numerator / $denominator } // warn "couldn't divide";
Where's the Catch???
In Perl 6, we aren't limited to just blindly catching a propagating exception and then coping with an undef. It is also possible to set up an explicit handler to catch, identify and deal with various types of exceptions. That's done in a CATCH block:
CATCH {
when Err::Reportable { warn $!; continue }
when Err::BadData { $!.fail(at=>$toknum) }
when NoData { push @stack, 0 }
when /division by zero/ { push @stack, Inf }
}
A CATCH block is like a BEGIN block (hence the capitalization). Its one argument is a closure that is executed if an exception ever propagates as far as the block in which the CATCH was declared. If the block eventually executes, then the current topic is aliased to the error variable $!. So the typical thing to do is to populate the exception handler's closure with a series of when statements that identify the exception contained in $! and handle the error appropriately. More on that in a moment.
The CATCH block has one additional property. When its closure has executed, it transfers control to the end of the block in which it was defined. This means that exception handling in Perl 6 is non-resumptive: once an exception is handled, control passes outward, and the code that threw the exception is not automatically re-executed.
If we did want "try, try, try again" exception handling instead, then we'd need to explicitly code a loop around the code we're trying:
# generate exceptions (sometimes)
sub getnum_or_die {
given <> { # readline and make it the topic
die "$_ is not a number"
unless defined && /^\d+$/;
return $_;
}
}
# non-resumptive exception handling
sub readnum_or_cry {
return getnum_or_die; # maybe generate an exception
CATCH { warn $! } # if so, warn and fall out of sub
}
# pseudo-resumptive
sub readnum_or_retry {
loop { # loop endlessly...
return getnum_or_die; # maybe generate an exception
CATCH { warn $! } # if so, warn and fall out of loop
} # (i.e. loop back and try again)
}
Note that this isn't true resumptive exception handling. Control still passes outward -- to the end of the loop block. But then the loop reiterates, sending control back into getnum_or_die for another attempt.
Catch as Catch Can
Within the CATCH block, the example uses the standard Perl 6 exception handling technique: a series of when statements. Those when statements compare their arguments against the current topic. In a CATCH block, that topic is always aliased to the error variable $!, which contains a reference to the propagating exception object.
The first three when statements use a classname as their argument. When matching a classname against an object, the =~ operator (and therefore any when statement) will call the object's isa method, passing it the classname. So the first three cases of the handler:
when Err::Reportable { warn $!; continue }
when Err::BadData { $!.fail(at=>$toknum) }
when NoData { push @stack, 0 }
are (almost) equivalent to:
if $!.isa(Err::Reportable) { warn $! }
elsif $!.isa(Err::BadData) { $!.fail(at=>$toknum) }
elsif $!.isa(NoData) { push @stack, 0 }
except far more readable.
The first when statement simply passes the exception object to warn.
Since warn takes a string as its argument, the exception object's stringification operator (inherited from the standard Exception class) is invoked and returns an appropriate diagnostic string, which is printed.
The when block then executes a continue statement, which circumvents the default "break out of the surrounding topicalizer block" semantics of the when.
The second when statement calls the propagating exception's fail method to cause calc either to return or rethrow the exception, depending on whether use fatal was set. In addition, it passes some extra information to the exception, namely the number of the token that caused the problem.
The third when statement handles the case where there is no cached data corresponding to the calculator's "previous" keyword, by simply
pushing a zero onto the stack.
The final case that the handler tests for:
when /division by zero/ { push @stack, Inf }
uses a regex, rather than a classname. This causes the topic (i.e. the exception) to be stringified and pattern-matched against the regex. As mentioned above, by default, all exceptions stringify to their own diagnostic string. So this part of the handler simply tests whether that string includes the words "division by zero," in which case it pushes the Perl 6 infinity value onto the stack.
One Dot Only
The CATCH block handled bad data by calling the fail method of the current exception:
when Err::BadData { $!.fail(at=>$toknum) }
That's a particular instance of a far more general activity: calling a method on the current topic. Perl 6 provides a shortcut for that -- the prefix unary dot operator. Unary dot calls the method that is its single operand, using the current topic as the implicit invocant. So the Err::BadData handler could have been written:
when Err::BadData { .fail(at=>$toknum) }
One of the main uses of unary dot is to allow when statements to select behavior on the basis of method calls. For example:
given $some_object {
when .has_data('new') { print "New data available\n" }
when .has_data('old') { print "Old data still available\n" }
when .is_updating { sleep 1 }
when .can('die') { .die("bad state") } # $some_object.die(...)
default { die "internal error" } # global die
}
Unary dot is also useful within the definition of methods themselves. In a Perl 6 method, the invocant (i.e. the first argument of the method, which is a reference to the object on which the method was invoked) is always the topic, so instead of writing:
method dogtag (Soldier $self) {
print $self.rank, " ", $self.name, "\n"
unless $self.status('covert');
}
we can just write:
method dogtag (Soldier $self) { # $self is automagically the topic
print .rank, " ", .name, "\n"
unless .status('covert');
}
or even just:
method dogtag { # @_[0] is automagically the topic
print .rank, " ", .name, "\n"
unless .status('covert');
}
Yet another use of unary dot is as a way of abbreviating multiple accesses to hash or array elements. That is, given also implements the oft-coveted with statement. If many elements of a hash or array are to be accessed in a set of statements, then we can avoid the tedious repetition of the container name:
# initialize from %options...
$name = %options{name} // %options{default_name};
$age = %options{age};
$limit = max(%options{limit}, %options{rate} * %options{count});
$count = $limit / %options{max_per_count};
by making it the topic and using unary dot:
# initialize from %options...
given %options {
$name = .{name} // .{default_name};
$age = .{age};
$limit = max(.{limit}, .{rate} * .{count});
$count = $limit / .{max_per_count};
}

