The Awesome Errors of Perl 6
If you're following tech stuff, you probably know by now about the folks at Rust land working on some totally awesome error reporting capabilities. Since Perl 6 is also known for its Awesome Errors, mst inquired for some examples to show off to the rustaceans, and unfortunately I drew a blank...
Errors are something I try to avoid and rarely read in full. So I figured I'll hunt down some cool examples of Awesome Errors and write about them. While I could just bash my head on the keyboard and paste the output, that'd be quite boring to read, so I'll talk about some of the tricky errors that might not be obvious to beginners, and how to fix them.
Let the head bashing begin!
The Basics
Here's some code with an error in it;
say "Hello world!;
say "Local time is {DateTime.now}";
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Two terms in a row (runaway multi-line "" quote starting at line 1 maybe?)
# at /home/zoffix/test.p6:2
# ------> say "⏏Local time is {DateTime.now}";
# expecting any of:
# infix
# infix stopper
# postfix
# statement end
# statement modifier
# statement modifier loop
The first line is missing the closing quote on the string, so everything until the opening quote on the second line is still considered part of the string. Once the supposedly closing quote is found, Perl 6 sees word "Local," which it identifies as a term. Since two terms in a row are not allowed in Perl 6, the compiler throws an error, offering some suggestions on what it was expecting, and it detects we're in a string and suggests we check we didn't forget a closing quote on line 1.
The ===SORRY!===
part doesn't mean you're running the Canadian version
of the compiler, but rather that the error is a compile-time (as compared
to a run-time) error.
Nom-nom-nom-nom
Here's an amusing error. We have a subroutine that returns things, so we call
it and use a for
loop to iterate over the values:
sub things { 1 … ∞ }
for things {
say "Current stuff is $_";
}
# ===SORRY!===
# Function 'things' needs parens to avoid gobbling block
# at /home/zoffix/test.p6:5
# ------> }⏏<EOL>
# Missing block (apparently claimed by 'things')
# at /home/zoffix/test.p6:5
# ------> }⏏<EOL>
Perl 6 lets you omit parentheses when calling subroutines. The error talks
about gobbling blocks. What happens is the block we were hoping to
give to the for
loop is actually being passed to the subroutine as an
argument instead. The second error in the output corroborates by saying
the for
loop is missing its block (and makes a suggestion it was taken by
the our things
subroutine).
The first error tells us how to fix the issue: Function 'things' needs parens
,
so our loop needs to be:
for things() {
say "Current stuff is $_";
}
However, were our subroutine actually expecting a block to be passed, no
parentheses would be necessary. Two code blocks side by side would result in
"two terms in a row" error we've seen above, so Perl 6 knows to pass the first
block to the subroutine and use the second block as the body of the for
loop:
sub things (&code) { code }
for things { 1 … ∞ } {
say "Current stuff is $_";
}
Did You Mean Levestein?
Here's a cool feature that will not only tell you you're wrong, but also point out what you might've meant:
sub levenshtein {}
levestein;
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Undeclared routine:
# levestein used at line 2. Did you mean 'levenshtein'?
When Perl 6 encounters names it doesn't recognize it computes Levenshtein distance for the things it does know to try to offer a useful suggestion. In the instance above it encountered an invocation of a subroutine it didn't know about. It noticed we do have a similar subroutine, so it offered it as an alternative. No more staring at the screen, trying to figure out where you made the typo!
The feature doesn't consider everything under the moon each time it's
triggered, however. Were we to capitalize the sub's name to Levenshtein
, we
would no longer get the suggestion, because for things that start with a
capital letter, the compiler figures it's likely a type name and not a
subroutine name, so it checks for those instead:
class Levenshtein {}
Lvnshtein.new;
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Undeclared name:
# Lvnshtein used at line 2. Did you mean 'Levenshtein'?
Once You Go Seq, You Never Go Back
Let's say you make a short sequence of Fibonacci numbers. You print it and then you'd like to print it again, but this time square each member. What happens?
my $seq = (1, 1, * + * … * > 100);
$seq .join(', ').say;
$seq.map({ $_² }).join(', ').say;
# 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144
# This Seq has already been iterated, and its values consumed
# (you might solve this by adding .cache on usages of the Seq, or
# by assigning the Seq into an array)
# in block <unit> at test.p6 line 3
Ouch! A run-time error. What's happening is the Seq
type
we get from the the sequence
operator
doesn't keep stuff around. When you iterate over it, each time it gives you
a value, it discards it, so once you've iterated over the entire Seq
, you're
done.
Above, we're attempting to iterate over it again, and so the Rakudo runtime
cries and complains, because it can't do it. The error message does offer two
possible solutions.
We can either use the .cache
method
to obtain a List
we'll iterate over:
my $seq = (1, 1, * + * … * > 100).cache;
$seq .join(', ').say;
$seq.map({ $_² }).join(', ').say;
Or we can use an Array from the get go:
my @seq = 1, 1, * + * … * > 100;
@seq .join(', ').say;
@seq.map({ $_² }).join(', ').say;
And even though we're storing the Seq
in an Array
, it won't get reified
until it's actually needed:
my @a = 1 … ∞;
say @a[^10];
# OUTPUT:
# (1 2 3 4 5 6 7 8 9 10)
These Aren't The Attributes You're Looking For
Imagine you have a class. In it, you have some private attributes and you've got a method that does a regex match using the value of that attribute as part of it:
class {
has $!prefix = 'foo';
method has-prefix ($text) {
so $text ~~ /^ $!prefix/;
}
}.new.has-prefix('foobar').say;
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Attribute $!prefix not available inside of a regex, since regexes are methods on Cursor.
# Consider storing the attribute in a lexical, and using that in the regex.
# at /home/zoffix/test.p6:4
# ------> so $text ~~ /^ $!prefix⏏/;
# expecting any of:
# infix stopper
Oops! What happened?
It's useful to understand that as far as the parser is concerned, Perl 6 is actually braided from several languages: Perl 6, Quote, and Regex languages are parts of that braiding. This is why stuff like this Just Works™:
say "foo { "bar" ~ "meow" } ber ";
# OUTPUT:
# foo barmeow ber
Despite the interpolated code block using the same "
quotes to delimit the
strings within it as the quotes on our original string, there's no conflict.
However, the same mechanism presents us with a limitation in regexes, because
in them, the looked up attributes belong to the Cursor
object responsible
for the regex.
To avoid the error, simply use a temporary variable to store the $!prefix
in—as suggested by the error message—or use the given
block:
class {
has $!prefix = 'foo';
method has-prefix ($text) {
given $!prefix { so $text ~~ /^ $_/ }
}
}.new.has-prefix('foobar').say;
De-Ranged
Ever tried to access an element of a list that's out of range?
my @a = <foo bar ber>;
say @a[*-42];
# Effective index out of range. Is: -39, should be in 0..Inf
# in block <unit> at test.p6 line 2
In Perl 6, to index an item from the end of a list, you use funky syntax:
[*-42]
. That's actually a closure that takes an argument (which is the
number of elements in the list), subtracts 42 from it, and the return value
is used as an actual index. You could use @a[sub ($total) { $total - 42 }]
instead, if you were particularly bored.
In the error above, that index ends up being 3 - 42
, or -39
, which is
the value we see in the error message. Since indexes cannot be negative,
we receive the error, which also tells us the index must be 0 to positive
infinity (with any indexes above what the list contains returning Any
when
looked up).
A Rose By Any Other Name, Would Code As Sweet
If you're an active user of Perl 6's sister language, the Perl 5, you may sometimes write Perl-5-isms in your Perl 6 code:
say "foo" . "bar";
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Unsupported use of . to concatenate strings; in Perl 6 please use ~
# at /home/zoffix/test.p6:1
# ------> say "foo" .⏏ "bar";
Above, we're attempting to use Perl 5's concatenation operator to concatenate
two strings. The error mechanism is smart enough to detect such usage
and to recommend the use of the correct ~
operator instead.
This is not the only case of such detection. There are many. Here's another example, detecting accidental use of Perl 5's diamond operator, with several suggestions of what the programmer may have meant:
while <> {}
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Unsupported use of <>; in Perl 6 please use lines() to read input, ('') to
# represent a null string or () to represent an empty list
# at /home/zoffix/test.p6:1
# ------> while <⏏> {}
Heredoc, Theredoc, Everywheredoc
Here's an evil error and there's nothing awesome about it, but I figured I'd mention it, since it's easy to debug if you know about it, and quite annoying if you don't. The error is evil enough that it may have been already improved if you're reading this far enough in the future from when I'm writing this.
Try to spot what the problem is... read the error at the bottom first, as if you were the one who wrote (and so, are familiar with) the code:
my $stuff = qq:to/END/;
Blah blah blah
END;
for ^10 {
say 'things';
}
for ^20 {
say 'moar things';
}
sub foo ($wtf) {
say 'oh my!';
}
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Variable '$wtf' is not declared
# at /home/zoffix/test.p6:13
# ------> sub foo (⏏$wtf) {
Huh? It's crying about an undeclared variable, but it's pointing to a signature of a subroutine. Of course it won't be declared. What sort of e-Crack is the compiler smoking?
For those who didn't spot the problem: it's the spurious semicolon after
the closing END
of the heredoc. The heredoc ends where the closing delimiter
appears on a line all by itself. As far as the compiler is concerned, we've
not seen the delimiter
at END;
, so it continues parsing as if it were still parsing the heredoc.
A qq
heredoc lets you interpolate variables, so when the parser gets to the
$wtf
variable in the signature, it has no idea it's in a signature of an
actual code and not just some random text, so it cries about the variable
being undeclared.
Won't Someone Think of The Reader?
Here's a great error that prevents you from writing horrid code:
my $a;
sub {
$a.say;
$^a.say;
}
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# $a has already been used as a non-placeholder in the surrounding sub block,
# so you will confuse the reader if you suddenly declare $^a here
# at /home/zoffix/test.p6:4
# ------> $^a⏏.say;
Here's a bit of a background: you can use the
$^
twigil
on variables to create an implicit signature. To make it possible to use such
variables in nested blocks, this syntax actually creates the same variable
without the twigil, so $^a
and $a
are the same thing, and the signature
of the sub above is ($a)
.
In our code, we also have an $a
in outer scope and supposedly we print it
first, before using the $^
twigil to create another $a
in the same scope,
but one that contains the argument to the sub... complete brain-screw! To
avoid this, just rename your variables to something that doesn't clash. How
about some Thai?
my $ความสงบ = 'peace';
sub {
$ความสงบ.say;
$^กับตัวแปรของคุณ.say;
}('to your variables');
# OUTPUT:
# peace
# to your variables
Well, Colour Me Errpressed!
If your terminal supports it, the compiler will emit ANSI codes to colourize the output a bit:
for ^5 {
say meow";
}
That's all nice and spiffy, but if you're, say, capturing output from the
compiler to display it elsewhere, you may get the ANSI code as is, like
31m===[0mSORRY![31m===[0m
.
That's awful, but luckily, it's easy to disable the colours: just set
RAKUDO_ERROR_COLOR
environmental variable to 0
:
You can set it from within the program too. You just have to do it early
enough, so put it somewhere at the start of the program and use the
BEGIN
phaser to set it
as soon as the assignment is compiled:
BEGIN %*ENV<RAKUDO_ERROR_COLOR> = 0;
for ^5 {
say meow";
}
An Exceptional Failure
Perl 6 has a special exception—Failure
—that
doesn't explode until you try to use it as a value, and you can even defuse it
entirely by using it in boolean context. You can produce your own Failures
by calling the fail
subroutine and
Perl 6 uses them in core whenever it can.
Here's a piece of code where we define a prefix operator for calculating the
circumference of an object, given its radius. If the radius is negative,
it calls fail
, returning a Failure
object:
sub prefix:<⟳> (\𝐫) {
𝐫 < 0 and fail 'Your object warps the Universe a new one';
τ × 𝐫;
}
say 'Calculating the circumference of the mystery object';
my $cₘ = ⟳ −𝑒;
say 'Calculating the circumference of the Earth';
my $cₑ = ⟳ 6.3781 × 10⁶;
say 'Calculating the circumference of the Sun';
my $cₛ = ⟳ 6.957 × 10⁸;
say "The circumference of the largest object is {max $cₘ, $cₑ, $cₛ} metres";
# OUTPUT:
# Calculating the circumference of the mystery object
# Calculating the circumference of the Earth
# Calculating the circumference of the Sun
# Your object warps the Universe a new one
# in sub prefix:<⟳> at test.p6 line 2
# in block <unit> at test.p6 line 7
#
# Actually thrown at:
# in block <unit> at test.p6 line 15
We're calculating the circumference for a negative radius on line 7, so if it were just a regular exception, our code would die there and then. Instead, by the output we can see that we continue to calculate the circumference of the Earth and the Sun, until we get to the last line.
There we try to use the Failure
in $cₘ
variable as one of the arguments to
the max
routine. Since we're asking for
the actual value, the Failure explodes and gives us a nice backtrace. The
error message includes the point where our Failure blew up (line 15), where
we received it (line 7) as well as where it came from (line 2). Pretty sweet!
Conclusion
Useful, descriptive errors are becoming the industry standard and Perl 6 and Rust languages are leading that effort. The errors must go beyond merely telling you the line number to look at. They should point to a piece of code you wrote. They should make a guess at what you meant. They should be referencing your code, even if they originate in some third party library you're using.
Most of Perl 6 errors display the piece of code containing the error. They use algorithms to offer valid suggestions when you mistyped a subroutine name. If you're used to other languages, Perl 6 will detect your "accent," and offer the correct way to pronounce your code in Perl 6. And instead of immediately blowing up, Perl 6 offers a mechanism to propagate errors right to the code the programmer is writing.
Perl 6 has Awesome Errors.
Leave a comment