'Returning values from exception handlers in Perl 6
I've been trying to write a Perl 6 expression which performs the following logic: Evaluate a subexpression and return its value, but if doing so causes an exception to be raised, catch the exception and return a fixed value instead.
For example, suppose I want to divide two numbers and have the expression evaluate to -1 if an error occurs. In Ruby I might write:
quotient = begin; a / b; rescue; -1; end
In Emacs Lisp that might be written as:
(setq quotient (condition-case nil (/ a b) (error -1))
My first Perl 6 attempt was like so:
sub might-throw($a, $b) { die "Zero" if $b == 0; $a / $b }
my $quotient = do { might-throw($a, $b); CATCH { default { -1 } } };
But here $quotient ends up undefined, regardless of whether $b is zero.
It seems that that the value returned by CATCH is ignored, or at least on the doc page that describes how exceptions work, all of the CATCH bodies only do things with side effects, like logging.
That page mentions try as an alternative. I might write for example:
my $quotient = try { might-throw($a, $b) } // -1;
I find it a rather underwhelming solution. For one thing, the expression I'm evaluating might genuinely have an undefined value, and I can't distinguish this from the case where an exception was thrown. For another, I might want to fall back to different values depending on the class of the thrown exception, but try just swallows them all. I can put my own CATCH block in the try to distinguish among the exceptions, but then I'm back at the first case above, where the value from the CATCH is ignored.
Can Perl 6's exception handling do as I've expressed I want it to be able to do above?
EDIT:
The current answers are informative, but are focusing too narrowly on the semantics of the division operator. I've rewritten the question slightly to make the main issue of exception catching more central.
Solution 1:[1]
The reason your catch block doesn't work is because dividing by zero isn't in and of itself an error. Perl6 will happily let you divide by zero and will store that value as a Rat. The issue arises when you want to display said Rat in a useful fashion (IE say it). That's when you get a Failure returned that becomes and Exception if not handled.
So you've a few options. You can check $b before you make $q :
$q = $b == 0 ?? -1 !! $a / $b;
Or if you want to keep the real value (note you can introspect both the numerator and the denominator of a Rat without causing the divide by Zero error) when you say it you can use the .perl or .Num versions.
Both give you the decimal representation of the Rat with .perl giving <1/0> and .Num giving Inf when you have a 0 denominator.
Solution 2:[2]
TL;DR The bulk of this answer introduces trys, my solution comprehensively addressing the overall issue your Q demonstrates and much more besides. The last section discusses some things happening in your attempts that others failed to address[1 2].
trys summary
A couple very simple examples:
say trys { die }, { -1 } # -1
say trys { die }, { when X::AdHoc { 42 } } # 42
trys is a single user defined routine that combines the best of the built in try and CATCH constructs. It:
Takes a list of one or more
Callables (functions, lambdas, etc), each of which can play either atryrole, aCATCHrole, or both.Passes the "ambient" (last) exception to each
Callableas its topic.Calls each
Callablein turn until one succeeds or they all "fail" (throw exceptions or otherwise reject a result).Returns a value, either the result of the first successful call of a
Callableor aFailurethat wraps the exception thrown by the lastCallable(or all exceptions if optional:$all-throwsis passed).Is not a spelling mistake.[3]
The trys code
unit module X2;
our sub trys ( **@callables, #= List of callables.
:$reject = (), #= Value(s) to be rejected.
:$all-throws = False, #= Return *all* thrown exceptions?
:$HANDLED = True, #= Mark returned `Failure` handled?
) is export {
my @throws; #= For storing all throws if `$all-throws`.
$! = CLIENT::<$!>; # First callable's `$!` is `trys` caller's.
@throws.push: $! if $! && $all-throws; # Include caller's `$!` in list of throws.
my $result is default(Nil); # At least temporarily preserve a `Nil` result.
for @callables -> &callable {
$result = try { callable $! } # `try` next callable, passing `$!` from prior callable as topic.
if not $! and $result ~~ $reject.any # Promote result to exception?
{ $! = X::AdHoc.new: payload => "Rejected $result.gist()" }
@throws.push: $! if $! && $all-throws;
return $result if not $!; # Return result if callable didn't throw.
}
$! = X::AdHoc.new: payload => @throws if $all-throws;
given Failure.new: $! { # Convert exception(s) to `Failure`.
.handled = $HANDLED;
.return
}
}
Code on glot.io (includes all trys code in this answer).
trys in detail
use X2;
# `trys` tries a list of callables, short circuiting if one "works":
say trys {die}, {42}, {fail} # 42
# By default, "works" means no exception thrown and result is not a `Failure`:
say trys {die}, {fail}, {42} # 42
# An (optional) `:reject` argument lets you specify
# value(s) you want rejected if they smartmatch:
say trys :reject(Nil,/o/), {Nil}, {'no'}, {2} # 2
# If all callables throw, return `Failure` wrapping exceptions(s):
say trys :reject(Nil), {Nil} # (HANDLED) Rejected Nil
say trys {die} # (HANDLED) Died
say trys {(42/0).Str} # (HANDLED) Attempt to divide by zero
# Specify `:!HANDLED` if the returned `Failure` is to be left unhandled:
say (trys {(42/0).Str}, :!HANDLED) .handled; # False
# The first callable is passed the caller's current exception as its topic:
$! = X::AdHoc.new: payload => 'foo';
trys {.say} # foo
# Topic of subsequent callables is exception from prior failed callable:
trys {die 'bar'}, *.say; # bar
trys {fail 'bar'}, {die "$_ baz"}, *.say; # bar baz
# Caller's `$!` is left alone (presuming no `trys` bug):
say $!; # foo
# To include *all* throws in `Failure`, specify `:all-throws`:
say trys {die 1}, {die 2}, :all-throws; # (HANDLED) foo 1 2
# Note the `foo` -- `all-throws` includes the caller's original `$!`.
trys "traps"
# Some "traps" are specific to the way `trys` works:
say trys { ... } // 42; # "(HANDLED) Stub code executed"
say trys { ... }, { 42 } # 42 <-- List of blocks, no `//`.
#trys 22; # Type check failed ... got Int (22)
say trys { 22 } # 22 <-- Block, not statement.
#trys {} # Type check failed ... got Hash ({})
say trys {;} # Nil <-- Block, not Hash.
# Other "traps" are due to the way Raku works:
# WAT `False` result if callable has `when`s but none match:
say do {when rand { 42 }} # False <-- It's how Raku works.
say trys {when rand { 42 }} # False <-- So same with `trys`.
say trys {when rand { 42 }; Nil} # Nil <-- Succinct fix.
say trys {when rand { 42 }; default {}} # Nil <-- Verbose fix.
# Surprise `(Any)` result if callable's last/return value is explicitly `$!`:
$! = X::AdHoc.new: payload => 'foo';
say try {$!} # (Any) <-- Builtin `try` clears `$!`.
say $!; # (Any) <-- Caller's too!
$! = X::AdHoc.new: payload => 'foo';
say trys {$!} # (Any) <-- `trys` clears `$!` BUT:
say $!; # foo <-- Caller's `$!` left alone.
$! = X::AdHoc.new: payload => 'foo';
say try {$!.self} # foo <-- A fix with builtin `try`.
say $!; # (Any) <-- Caller's `$!` still gone.
$! = X::AdHoc.new: payload => 'foo';
say trys {.self} # foo <-- Similar fix with `trys`.
say $!; # foo <-- Caller's `$!` left alone.
Discussion of your attempts
My first Raku attempt was like so:
sub might-throw($a, $b) { die "Zero" if $b == 0; $a / $b }
my $quotient = do { might-throw($a, $b); CATCH { default { -1 } } };
A CATCH block always returns Nil. It's the last statement in the closure body so a Nil is always returned. (This is a footgun that plausibly ought be fixed. See further discussion in Actually CATCHing exceptions without creating GOTO)
I might write for example:
my $quotient = try { might-throw($a, $b) } // -1;
the expression I'm evaluating might genuinely have an undefined value, and I can't distinguish this from the case where an exception was thrown.
You could instead write:
my $quotient is default(-1) = try { might-throw($a, $b) }
What's going on here:
The
is defaulttrait declares what a variable's default value is, which is used if it's not initialized and also if there's an attempt to assignNil. (WhileNilis technically an undefined value, its purpose is to denote "Absence of a value or benign failure".)tryis defined to returnNilif an exception is thrown during its evaluation.
This may still be unsatisfactory if one wants to distinguish between a Nil that's returned due to an exception being thrown and one due to ordinary return of a Nil. Or, perhaps more importantly:
I might want to fall back to different values depending on the class of the thrown exception, but
tryjust swallows them all.
This needs a solution, but not CATCH:
I can put my own
CATCHblock in thetryto distinguish among the exceptions, but then I'm back at the first case above
Instead, there's now the trys function I've created.
Footnotes
[1] As you noted: "The current answers ... are focusing too narrowly on the semantics of the division operator.". So I've footnoted my summary of that aspect, to wit: to support advanced math, Raku doesn't automatically treat a rational divide by zero (eg 1/0) as an exception / error. Raku's consequent double delayed exception handling is a red herring.
[2]CATCH is also a red herring. It doesn't return a value, or inject a value, even when used with .resume, so it's the wrong tool for doing the job that needs to be done.
[3] Some might think trys would best be spelled tries. But I've deliberately spelled it trys. Why? Because:
In English, to the degree the the word
triesis related totry, it's very closely related. The sheer oddness of the word choicetrysis intended to remind folk it's not just a pluraltry. That said, the rough meaning is somewhat closely related totry, so spelling ittrysstill makes sense imo.I like whimsy. Apparently, in Albanian,
trysmeans "to press, compress, squeeze". Liketry, thetrysfunction "presses" code ("to press" in the sense of "to pressure"), and "compresses" it (as compared to the verbosity of not usingtrys), and "squeezes" all the exception related error mechanisms --Exceptions,Failures,Nils,try,CATCH,.resume-- into one.In Lithuanian,
trysmeans "three".trys:Rejects results of three kinds:
Exceptions;Failures; and user specified:rejectvalues.Keeps things rolling in three ways: passes caller's
$!to the first callable; calls subsequent callables with last exception as their topic; turns an exception thrown in the last block into aFailure.Tackles one of the hardest things in programming -- naming things:
trysis similar to but different fromtryin an obvious way; I hereby predict few devs will use the Albanian or Lithuanian wordstrysin their code; choosingtrysinstead oftriesmakes it less likely to clash with existing code. :)
Solution 3:[3]
This seems to be a design and/or implementation defect:
Rakudo happily divides an Int by 0, returning a Rat. You can .Num it (yielding Inf) and .perl it, but it will blow up if you try to .Str or .gist it.
In contrast, dividing by the Num 0e0 will fail immediately.
For the sake of consistency, integer division by zero should probably fail as well. The alternative would be returning a regular value that doesn't blow up when stringified, but I'd argue against it...
Solution 4:[4]
I got the following to work:
use v6;
my $a = 1;
my $b = 0;
my $quotient = $a / $b;
try {
#$quotient; # <-- Strangely, this does not work
"$quotient";
CATCH {
when X::Numeric::DivideByZero {
$quotient = -1;
}
default { fail }
}
}
say "Value of quotient: ", $quotient;
Output:
Value of quotient: -1
However, if I don't stringify $quotient in the try clause, it instead gives
Useless use of $quotient in sink context (line 9)
Attempt to divide 1 by zero using div
in block <unit> at ./p.p6 line 18
I am not sure if this can be a bug..
Edit:
To address the question of the return value from the CATCH block. You can work around the issue that it does not return a value to the outer scope by instead calling the resume method:
my $a = 1;
my $b = 0;
my $quotient = do {
my $result = might-throw($a, $b);
CATCH {
default {
say "Caught exception: ", .^name;
.resume;
}
}
$result; #<-- NOTE: If I comment out this line, it does not work
# A bug?
};
sub might-throw($a, $b) {
if $b == 0 {
die "Zero";
-1; # <-- the resume method call from CATCH will continue here
}
else {
$a / $b
}
}
Solution 5:[5]
So we've got a function. Sometimes it returns Any (undef) other wise is return $a / $b unless $b is 0 in which case it throws an exception.
sub might-throw($a, $b) {
return Any if (True, False, False, False, False).pick();
die "Zero" if $b == 0;
$a / $b;
}
We want quotient to be the value of the function call unless it throws an exception, in which case we want -1.
Lets make 20 random pairs and try it out :
for 1..20 {
my $a = (0..2).pick;
my $b = (0..2).pick;
my $quotient = -1;
try {
let $quotient = might-throw($a, $b);
$quotient ~~ Any|Numeric;
}
say "{$a}/{$b} is {$quotient} maybe..";
}
So we start be predefining the quotient to the error state. Then in a try block we call out function using let to set it. the let will be rolled back if the function errors or the block returns undef... Hence we test that $quotient is an Any or a Numeric.
Solution 6:[6]
Other answers have helpfully focused on the "why", so here's one focused just on the "how".
You asked how to rewrite
sub might-throw($a, $b) { die "Zero" if $b == 0; $a / $b }
my $quotient = do { might-throw($a, $b); CATCH { default { -1 } } };
so that it sets $quotient to the provided default when $b == 0. Here are two ways:
Option 1
sub might-throw($a, $b) { die "Zero" if $b == 0; $a / $b }
my $quotient = sub { might-throw($a, $b); CATCH { default { return -1 } } }();
Option 2
sub might-throw1($a, $b) { die "Zero" if $b == 0; $a / $b }
my $quotient = do with try might-throw1($a, $b) { $_ } elsif $! { -1 };
A few explanatory notes: CATCH blocks (and phasers more generally) do not implicitly return their last expression. You can explicitly return with the return function, but only from within a Routine (that is, a method or a sub). Option 1 wraps the block you provided in an immediately invoked anonymous sub, which gives you a scope from which to return.
Option 2 (which would be my preference) switches to try but addresses the two problems you noted with the try {…} // $default approach by taking advantage of the fact that try sets the value of $! to the last exception it caught.
You mentioned two problems with try {…} // $default. First, that you want to distinguish between an exception and a genuinely undefined value. Option 2 does this by testing whether try captured an exception – if &might-throw returned an undefined value without throwing an exception, $quotient will be undefined.
Second, you said that you "might want to fall back to different values depending on the class of the thrown exception". Option 2 could be extended to do this by matching on $! within the elsif block.
Solution 7:[7]
I think that creating an infix operator would make some sense.
sub infix:<rescue> ( $l, $r ) {
# return right side if there is an exception
CATCH { default { return $r }}
return $r if $l ~~ Nil; # includes Failure objects
return $r if $l == NaN;
# try to get it to throw an exception
sink $l;
sink ~$l; # 0/0
# if none of those tests fail, return the left side
return $l;
}
A quick copy of what you have into using this new operator:
my ($a,$b) = 0,0;
my $quotient = do { try { $a / $b } rescue -1 };
Which can of course be simplified to:
my $quotient = $a / $b rescue -1;
This doesn't cover the ability to have multiple typed rescue tests like Ruby.
(It wouldn't fit in with Raku if it did, plus CATCH already handles that.)
It also doesn't actually catch exceptions, so you would have to wrap the left side with try {…} if it could potentially result in one.
Of course once we get macros, that might be a whole other story.
(The best way to solve a problem is to create a language in which solving the problem is easy.)
If leave(value) were implemented you could maybe have used it in your CATCH block.
leave(value) as far as I know is supposed to be similar to return(value), which was part of the reason I used a sub.
do { might-throw($a, $b); CATCH { default { leave(-1) } } };
Though it might also not work because there are the two blocks created by CATCH {…} and default {…}.
This is all hypothetical anyway as it is not implemented.
If rescue were to actually be added to Raku, a new method might be in order.
use MONKEY-TYPING;
augment class Any {
proto method NEEDS-RESCUE ( --> Bool ){*}
multi method NEEDS-RESCUE ( --> False ){} # includes undefined
}
augment class Nil { # includes Failure objects
multi method NEEDS-RESCUE ( --> True ){}
}
# would be in the Rational role instead
augment class Rat {
multi method NEEDS-RESCUE (Rat:D: ){
$!denominator == 0
}
}
augment class FatRat {
multi method NEEDS-RESCUE (FatRat:D: ){
$!denominator == 0
}
}
augment class Num {
multi method NEEDS-RESCUE (Num:D: ){
self.isNAN
}
}
sub infix:<rescue> ( $l, $r ){
$l.NEEDS-RESCUE ?? $l !! $r
}
say 0/0 rescue -1; # -1
say 0/1 rescue -1; # 0
say NaN rescue -1; # -1
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Scimon Proctor |
| Solution 2 | |
| Solution 3 | Christoph |
| Solution 4 | |
| Solution 5 | Scimon Proctor |
| Solution 6 | codesections |
| Solution 7 | Brad Gilbert |
