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.
Read or Die
Having prompted for the next expression that the calculator will evaluate:
print "$i> ";
we read in the expression and check for an EOF (which will cause the <>
operator to return undef, in which case we escape the infinite loop):
my $expr = <> err last;
Err...err???
In Apocalypse 3, Larry introduced the // operator, which is like a || that tests its left operand for
definedness rather than truth.
What he didn't mention (but which you probably guessed) was that there is also the low-precedence version of //. Its name is err:
Operation High Precedence Low Precedence
INCLUSIVE OR || or
EXCLUSIVE OR ~~ xor
DEFINED OR // err
But why call it err?
Well, the // operator looks like a skewed version of ||, so the low-precedence version should probably be a skewed version of or. We can't skew it visually (even Larry thought that using italics would be going a bit far), so we skew it phonetically instead: or -> err.
err also has the two handy mnemonic connotations:
-
That we're handling an error marker (which a returned
undefusually is) -
That we're voicing a surprised double-take after something unexpected (which a returned
undefoften is).
Besides all that, it just seems to work well. That is, something like this:
my $value = compute_value(@args)
err die "Was expecting a defined value";
reads quite naturally in English (whether you think of err as an
abbreviation of "on error...", or as a synonym for "oops...").
Note that err is a binary operator, just like or, and xor,
so there's no particular need to start it on a new line:
my $value = compute_value(@args) err die "Was expecting a defined value";
In our example program, the undef returned by the <> operator at
end-of-file is our signal to jump out of the main loop. To accomplish that we
simply append err last to the input statement:
my $expr = <> err last;
Note that an or last wouldn't work here, as both the empty string and the
string "0" are valid (i.e. non-terminating) inputs to the calculator.
Just Do It
Then it's just a matter of calling Calc::calc, passing it the iteration
number and the expression:
Calc::calc(i=>$i, expr=>$expr)
Note that we used named arguments, so passing them in the wrong order didn't matter.
We then interpolate the result back into the output string using the $(...)
scalar interpolator:
print "$i> $( Calc::calc(i=>$i, expr=>$expr) )\n";
We could even simplify that a little further, by taking advatage of the fact
that subroutine calls interpolate directly into strings in Perl 6, provided we
use the & prefix:
print "$i> &Calc::calc(i=>$i, expr=>$expr)\n";
Either way, that's it: we're done.
Summing Up
In terms of control structures, Perl 6:
- provides far more support for exceptions and exception handling,
-
cleans up and extends the
forloop syntax in several ways, - unifies the notions of blocks and closures and makes them interchangeable,
- provides hooks for attaching various kinds of automatic handlers to a block/closure,
- re-factors the concept of a switch statement into two far more general ideas: marking a value/variable as the current topic, and then doing "smart matching" against that topic.
These extensions and cleanups offer us far more power and control, and -- amazingly -- in most cases require far less syntax. For example, here's (almost) the same program, written in Perl 5:
package Err::BadData;
use base 'Exception'; # which you'd have to write yourself
package NoData; # not lexical
use base 'Exception';
sub warn { die @_ }
package Calc;
my %var;
sub get_data {
my $data = shift;
if ($data =~ /^\d+$/) { return $var{""} = $data }
elsif ($data eq 'previous') { return defined $var{""}
? $var{""}
: die NoData->new()
}
elsif ($var{$data}) { return $var{""} = $var{$data} }
else { die Err::BadData->new(
{msg=>"Don't understand $data"}
)
}
}
sub calc {
my %data = @_;
my ($i, $expr) = @data{'i', 'expr'};
my %operator = (
'*' => sub { $_[0] * $_[1] },
'/' => sub { $_[0] / $_[1] },
'~' => sub { ($_[0] + $_[1]) / 2 },
);
my @stack;
my $toknum = 1;
LOOP: for my $token (split /\s+/, $expr) {
defined eval {
TRY: if ($operator{$token}) {
my @args = splice @stack, -2;
push @stack, $operator{$token}->(@args);
last TRY;
}
last LOOP if $token eq '.' || $token eq ';' || $token eq '=';
push @stack, get_data($token);
} || do {
if ($@->isa(Err::Reportable)) { warn $@; }
if ($@->isa(Err::BadData)) { $@->{at} = $i; die $@ }
elsif ($@->isa(NoData)) { push @stack, 0 }
elsif ($@ =~ /division by zero/) { push @stack, ~0 }
}
}
continue { $toknum++ }
die Err::BadData->new(msg=>"Too many operands") if @stack > 1;
$var{'$'.$i} = $stack[-1] . ' but true';
return 0+pop(@stack);
}
package main;
for (my $i=1; 1; $i++) {
print "$i> ";
defined( my $expr = <> ) or last;
print "$i> ${\Calc::calc(i=>$i, expr=>$expr)}\n";
}
Hmmmmmmm. I know which version I'd rather maintain.

