11

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.

Scimon Proctor
  • 4,558
  • 23
  • 22
Sean
  • 29,130
  • 4
  • 80
  • 105

7 Answers7

7

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.

Scimon Proctor
  • 4,558
  • 23
  • 22
7

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

raiph
  • 31,607
  • 3
  • 62
  • 111
  • Wait, did you publish it somewhere? – jjmerelo Mar 08 '21 at 09:20
  • 1
    @jjmerelo It's just in this SO. I think it might be the first thing I've written that perhaps *ought* be widely published in some other form, but I currently don't think it warrants doing so as the only thing in an ecosystem package. – raiph Mar 08 '21 at 10:11
  • @jjmerelo I didn't elaborate why not. But the comment `# No idea if this is reliable.` was a part of it. It no longer works. Currently a much simpler incantation works. But again, I've no idea if it's reliable. I might post about that one day, but I'll likely wait till after jnthn has landed his dispatch and rakuast work and had time to take a breather for maybe a while (a year?). All in good time... – raiph Jul 31 '21 at 15:29
3

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
    }
}
Håkon Hægland
  • 39,012
  • 21
  • 81
  • 174
  • 1
    `$result; #<-- NOTE: If I comment out this line, it does not work ... A bug?` Looks like it to me. I just [searched rt for resume](https://rt.perl.org/Public/Search/Simple.html?q=resume) and [GH issues for resume](https://github.com/rakudo/rakudo/issues?utf8=%E2%9C%93&q=is%3Aissue+resume) and didn't see a match. I also searched for catch and return in rt and GH. So I've looked at a LOT of bugs and none match this. Please report it if you have the time. Try to golf it. Does it require CATCH/resume? – raiph Aug 05 '18 at 18:43
  • 1
    `#$quotient; # <-- Strangely, this does not work`. Aiui, that's not a bug. `$quotient` is a `Scalar` container. It's in sink context, so it just shows a warning about useless use but sees no need to look inside `$quotient`, pull its value out, go "omg" and thence throw an exception. So neither of these involve a throw: `my $foo = Failure.new; $foo` nor `my $foo = Failure.new; say try { $foo; 42 }` whereas both these do `my $foo = Failure.new; "$foo"` and `say try { Failure.new; 42 }`. – raiph Aug 05 '18 at 18:44
  • @raiph Thanks for the comments! I have added a bug report on GitHub: [Missing return value from do when calling resume and CATCH is the last statement in a block](https://github.com/rakudo/rakudo/issues/2181) – Håkon Hægland Aug 05 '18 at 19:18
  • Thanks. In retrospect I must know this bug because I know not to put CATCH blocks at the end. But when I tested that before writing my comment I failed to duplicate a problem. Or maybe I confused myself. Anyhow, thanks for putting it on record, assuming I didn't manage to miss it as I looked at all those bugs. :) – raiph Aug 05 '18 at 19:24
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...

Christoph
  • 164,997
  • 36
  • 182
  • 240
  • A consistency *argument* is always a reasonable *argument*. [TimToady Bicarbonate](https://news.ycombinator.com/item?id=1609120). I've too much to say about this to fit in a comment here. Likewise I have other responses to other answers and OP. So I'm writing an answer. But that might take me a couple days. In the meantime, I want to register a *tentative* -1 to thinking of this as a design/implementation defect and +1 to viewing it as a teachable moment about fundamental differences between Rational and Floating point semantics; nice aspects of P6's exception handling; and other goodies. – raiph Aug 02 '18 at 16:45
  • @raiph: if it's supposed to yield a regular value, it shouldn't blow up when calling `.gist`, the same as you don't expect `.gist` to explode on a `Num` that's `Inf` or (non-signalling) `NaN`; that would be the implementation defect I was alluding to – Christoph Aug 02 '18 at 17:23
  • 1
    The OP's question is dead simple. I applaud attempts at a simple answer and in a sense hope one of us has nailed it. But I see the OP's question touching on many topics from exceptions to language design (and notably [language version detection](https://github.com/rakudo/rakudo/issues/1289) (note the final comment about IEEE `/`)). And numerics. Part of my point is that anything divided by zero is not a regular value. But I hear you. I'm just calling attention to both sides of meaning of the qualifier "seems" in your answer's first sentence. I think there's more to this than meets the eye. – raiph Aug 02 '18 at 18:45
  • @p6steve. Thus `say 1/0` dies. This topic is a red herring. It took me a while to write it but I've now published an answer that stays focused on the OP's point. Returning to `1/0`, it's a numeric literal meaning `1` over `zero`. This has a very useful spot [in mathematics](https://en.wikipedia.org/wiki/Division_by_zero) and computation. The IEEE standard for floats mandates that `n/0` and `-n/0` are not death but [+∞ and −∞](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Infinities). P6 doesn't yet do that but that's the standard. There's so much more to `1/0` than meets the eye. – raiph Aug 05 '18 at 16:44
  • `(1/0).Num === Inf;` `Inf.Rat === 1/0;` `(-1/0).Num === -Inf;` `-Inf.Rat === -1/0;` `(0/0).Num === NaN;` `NaN.Rat === 0/0;` – Brad Gilbert Mar 08 '21 at 19:52
3

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.

Scimon Proctor
  • 4,558
  • 23
  • 22
2

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.

codesections
  • 8,900
  • 16
  • 50
2

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
Brad Gilbert
  • 33,846
  • 11
  • 78
  • 129