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.
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
summaryA 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 Callable
s (functions, lambdas, etc), each of which can play either a try
role, a CATCH
role, or both.
Passes the "ambient" (last) exception to each Callable
as its topic.
Calls each Callable
in 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 Callable
or a Failure
that wraps the exception thrown by the last Callable
(or all exceptions if optional :$all-throws
is passed).
Is not a spelling mistake.[3]
trys
codeunit 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 detailuse 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.
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 default
trait declares what a variable's default value is, which is used if it's not initialized and also if there's an attempt to assign Nil
. (While Nil
is technically an undefined value, its purpose is to denote "Absence of a value or benign failure".)
try
is defined to return Nil
if 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
try
just swallows them all.
This needs a solution, but not CATCH
:
I can put my own
CATCH
block in thetry
to distinguish among the exceptions, but then I'm back at the first case above
Instead, there's now the trys
function I've created.
[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 tries
is related to try
, it's very closely related. The sheer oddness of the word choice trys
is intended to remind folk it's not just a plural try
. That said, the rough meaning is somewhat closely related to try
, so spelling it trys
still makes sense imo.
I like whimsy. Apparently, in Albanian, trys
means "to press, compress, squeeze". Like try
, the trys
function "presses" code ("to press" in the sense of "to pressure"), and "compresses" it (as compared to the verbosity of not using trys
), and "squeezes" all the exception related error mechanisms -- Exception
s, Failure
s, Nil
s, try
, CATCH
, .resume
-- into one.
In Lithuanian, trys
means "three". trys
:
Rejects results of three kinds: Exception
s; Failure
s; and user specified :reject
values.
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 a Failure
.
Tackles one of the hardest things in programming -- naming things: trys
is similar to but different from try
in an obvious way; I hereby predict few devs will use the Albanian or Lithuanian words trys
in their code; choosing trys
instead of tries
makes it less likely to clash with existing code. :)
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.
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...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With