Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 768
Sean Avatar asked Aug 02 '18 01:08

Sean


3 Answers

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 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]

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 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 the try 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.

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 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 -- Exceptions, Failures, Nils, try, CATCH, .resume -- into one.

  • In Lithuanian, trys means "three". trys:

    1. Rejects results of three kinds: Exceptions; Failures; and user specified :reject values.

    2. 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.

    3. 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. :)

like image 50
raiph Avatar answered Nov 11 '22 04:11

raiph


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.

like image 7
Scimon Proctor Avatar answered Nov 11 '22 04:11

Scimon Proctor


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...

like image 3
Christoph Avatar answered Nov 11 '22 04:11

Christoph