Skip Menu |

This queue is for tickets about the future CPAN distribution.

Report information
The Basics
Id: 103545
Status: open
Priority: 0/
Queue: future

People
Owner: Nobody in particular
Requestors: leonerd-cpan [...] leonerd.org.uk
Cc:
AdminCc:

Bug Information
Severity: Wishlist
Broken in: (no value)
Fixed in: (no value)



Subject: Design thoughts on ->catch method
A lot of uses of ->else or ->else_with_f turn out to be looking for specific kinds of failure. E.g. a common case is looking for HTTP failures: ->else_with_f( sub { my ( $f, undef, $name, $response ) = @_; return $f unless defined $name and $name eq "http"; ### maybe inspect $response here ... }) It would be nice in this case to be able to notate that boilerplate using ->catch( http => sub { my ( $message, $name, $response ) = @_; ... }) If 'catch' were to take a KV list instead of a single name/code pair, we could easily handle the "atomic" test of multiple failures without each getting in the way of the others, using ->catch( resolve => sub { die "Cannot resolve that hostname" }, connect => sub { die "Cannot connect to the host" }, ssl => sub { die "Failed to negotiate SSL" }, http => sub { die "Cannot talk HTTP" }, ... ) A few unresolved design questions come up here though: * How to notate failures without type names - e.g. the ones generated by internal perl failures (e.g. method call on undef, strictness violations, etc...) * How to 'catch' a done? The latter feature would be useful for handling "I expect this to fail" cases, where we want to turn certain specific kinds of failure into success, and success into a failure. For example, if we decide to "catch" non-failure using some special notation, then we could do ->catch( ':done' => sub { die "Expected HTTP 403 failure" }, http => sub { $_[2]->code == 403 and return Future->done; Future->fail( @_ ) }, ) Here, if the preceding Future fails with an "http" type of failure, and the response code was 403, then the sequenced Future will succeed; if the preceding one either succeeds or fails in some other way, then the sequenced one fails. Having such a notation as ':done' would suggest we might want to make it an error to Future->fail with a name field whose name starts with :. Moreover, it might make sense to suggest that the name field of any failure ought to match "sensible" identifier rules. -- Paul Evans
In addition to simple strings (to be C<eq>-matched against C<< ($f->failure)[0] >>), we could allow L<Smart::Match>-like objects (blessed refs overloading C<~~>) and C<Type::Tiny>-like objects (I'm not sure what the protocol is, here). C<':done'> still needs to be a bit special, since we'd not be matching the result of C<failure>; we can just use a unique ref blessed into a special package (e.g. C<<sub MATCH_DONE { state $x = bless do {\(my $a)},'Future::Catch::Special::Done'; $x }>>). Also, as tom suggested on IRC, we can allow regexes, and arrayrefs ("match any of the elements"). Half-assed implementation: sub catch { my $f = shift; my @cases; while (my ($case,$action) = splice @_,0,2) { push @cases, [_convert_matcher($case), $action]; } return $f->followed_by(sub { my $g = shift; for my $case (@cases) { my ($matcher,$action) = @_; if ($matcher->($g)) { return $action->($g); } } return $g; }); } sub _convert_matcher { my $case = shift; if (_is_smart_match($case)) { return sub { $_[0]->is_failed && ($_[0]->failure)[0] ~~ $case } } elsif (_is_type_constraint($case)) { return sub { $_[0]->is_failed && $case->check($_[0]->failure)[0]) } } elsif (_is_special_done_value($case)) { return sub { $_[0]->is_done } } elsif (ref($case) eq 'Regexp') { return sub { $_[0]->is_failed && ($_[0]->failure)[0] =~ $case } } elsif (ref($case) eq 'ARRAY') { my @subcases = map {_convert_matcher($_)} @$case; return sub { my ($f)=@_; any { $_->($f) } @subcases }; } else { die "wtf?" } }
Further discussions lead to the idea of: ->catch( @kvlist, $fallback ) to simply be a wrapper around a more generic idea of ->then( $on_success, @kvlist, $on_other_failure ) At which point we don't need the special "DONE" handler match. As to the kvlist: for now a simple string-equality match on the failure name, and the lack of statement about duplicates/ordering constraints, should let the implementation be quite flexible (for performance), while leaving the design space open for other ideas of better match abilities at some later point. -- Paul Evans
On Fri Apr 17 13:24:00 2015, PEVANS wrote: Show quoted text
> Further discussions lead to the idea of: > > ->catch( @kvlist, $fallback ) > > to simply be a wrapper around a more generic idea of > > ->then( $on_success, @kvlist, $on_other_failure ) > > At which point we don't need the special "DONE" handler match. > > As to the kvlist: for now a simple string-equality match on the > failure name, and the lack of statement about duplicates/ordering > constraints, should let the implementation be quite flexible (for > performance), while leaving the design space open for other ideas of > better match abilities at some later point.
An initial patch that implements it this far, and also points at this RT ticket for further consideration/discussion. I think this looks good enough to ship as a first cut. -- Paul Evans
Subject: rt103545-a1.patch
=== modified file 'lib/Future.pm' --- lib/Future.pm 2015-07-28 15:28:42 +0000 +++ lib/Future.pm 2015-07-28 18:38:51 +0000 @@ -76,6 +76,31 @@ See also L<Future::Utils> which contains useful loop-constructing functions, to run a future-returning function repeatedly in a loop. +=head2 FAILURE CATEGORIES + +While not directly required by C<Future> or its related modules, a growing +convention of C<Future>-using code is to encode extra semantics in the +arguments given to the C<fail> method, to represent different kinds of +failure. + +The convention is that after the initial message string as the first required +argument (intended for display to humans), the second argument is a short +lowercase string that relates in some way to the kind of failure that +occurred. Following this is a list of details about that kind of failure, +whose exact arrangement or structure are determined by the failure category. +For example, L<IO::Async> and L<Net::Async::HTTP> use this convention to +indicate at what stage a given HTTP request has failed: + + ->fail( $message, http => ... ) # an HTTP-level error during protocol + ->fail( $message, connect => ... ) # a TCP-level failure to connect a + # socket + ->fail( $message, resolve => ... ) # a resolver (likely DNS) failure + # to resolve a hostname + +By following this convention, a module remains consistent with other +C<Future>-based modules, and makes it easy for program logic to gracefully +handle and manage failures by use of the C<catch> method. + =head2 SUBCLASSING This class easily supports being subclassed to provide extra behavior, such as @@ -1079,9 +1104,11 @@ sub then { my $self = shift; - my ( $done_code, $fail_code ) = @_; + my $done_code = shift; + my $fail_code = ( @_ % 2 ) ? pop : undef; + my @catch_list = @_; - if( $done_code and !$fail_code ) { + if( $done_code and !@catch_list and !$fail_code ) { return $self->_sequence( $done_code, CB_SEQ_ONDONE|CB_RESULT ); } @@ -1091,6 +1118,7 @@ Carp::croak "Expected \$fail_code to be callable in ->then"; # Complex + my %catch_handlers = @catch_list; return $self->_sequence( sub { my $self = shift; if( !$self->{failure} ) { @@ -1098,6 +1126,10 @@ return $done_code->( $self->get ); } else { + my $name = $self->{failure}[1]; + if( defined $name and $catch_handlers{$name} ) { + return $catch_handlers{$name}->( $self->failure ); + } return $self unless $fail_code; return $fail_code->( $self->failure ); } @@ -1112,6 +1144,59 @@ return $self->_sequence( $fail_code, CB_SEQ_ONFAIL|CB_RESULT ); } +=head2 catch + + $future = $f1->catch( + name => \&code, + name => \&code, ... + ) + +Returns a new sequencing C<Future> that behaves like an C<else> call which +dispatches to a choice of several alternative handling functions depending on +the kind of failure that occurred. If C<$f1> fails with a category name (i.e. +the second argument to the C<fail> call) which exactly matches one of the +string names given, then the corresponding code is invoked, being passed the +same arguments as a plain C<else> call would take, and is expected to return a +C<Future> in the same way. + + $f2 = $code->( $exception, $name, @other_details ) + +If C<$f1> does not fail, fails without a category name at all, or fails with a +category name that does not match any given to the C<catch> method, then the +returned sequence future immediately completes with the same result, and no +block of code is invoked. + +If passed an odd-sized list, the final argument gives a function to invoke on +failure if no other handler matches. + + $future = $f1->catch( + name => \&code, ... + \&fail_code, + ) + +This feature is currently still a work-in-progress. It currently can only cope +with category names that are literal strings, which are all distinct. A later +version may define other kinds of match (e.g. regexp), may specify some sort +of ordering on the arguments, or any of several other semantic extensions. For +more detail on the ongoing design, see +L<https://rt.cpan.org/Ticket/Display.html?id=103545>. + +=head2 then I<(multiple arguments)> + + $future = $f1->then( \&done_code, @catch_list, \&fail_code ) + +The C<then> method can be passed an even-sized list inbetween the +C<$done_code> and the C<$fail_code>, with the same meaning as the C<catch> +method. + +=cut + +sub catch +{ + my $self = shift; + return $self->then( undef, @_ ); +} + =head2 transform $future = $f1->transform( %args ) === added file 't/07catch.t' --- t/07catch.t 1970-01-01 00:00:00 +0000 +++ t/07catch.t 2015-07-28 18:40:43 +0000 @@ -0,0 +1,94 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More; +use Test::Refcount; + +use Future; + +# catch success +{ + my $f1 = Future->new; + + my $fseq = $f1->catch( + test => sub { die "catch of successful Future should not be invoked" }, + ); + + ok( defined $fseq, '$fseq defined' ); + isa_ok( $fseq, "Future", '$fseq' ); + + is_oneref( $fseq, '$fseq has refcount 1 initially' ); + + $f1->done( results => "here" ); + + is_deeply( [ $fseq->get ], [ results => "here" ], '$fseq succeeds when $f1 succeeds' ); + + undef $f1; + is_oneref( $fseq, '$fseq has refcount 1 before EOF' ); +} + +# catch matching failure +{ + my $f1 = Future->new; + + my $f2; + my $fseq = $f1->catch( + test => sub { + is( $_[0], "f1 failure\n", 'catch block passed result of $f1' ); + return $f2 = Future->done; + }, + ); + + ok( defined $fseq, '$fseq defined' ); + isa_ok( $fseq, "Future", '$fseq' ); + + is_oneref( $fseq, '$fseq has refcount 1 initially' ); + + $f1->fail( "f1 failure\n", test => ); + + undef $f1; + is_oneref( $fseq, '$fseq has refcount 1 after $f1 fail and dropped' ); + + ok( defined $f2, '$f2 now defined after $f1 fails' ); + + ok( $fseq->is_ready, '$fseq is done after $f2 done' ); +} + +# catch non-matching failure +{ + my $f1 = Future->new; + + my $fseq = $f1->catch( + test => sub { die "catch of non-matching Failure should not be invoked" }, + ); + + $f1->fail( "f1 failure\n", different => ); + + ok( $fseq->is_ready, '$fseq is done after $f1 fail' ); + is( scalar $fseq->failure, "f1 failure\n", '$fseq failure' ); +} + +# catch default handler +{ + my $fseq = Future->fail( "failure", other => ) + ->catch( + test => sub { die "'test' catch should not match" }, + sub { Future->done( default => "handler" ) }, + ); + + is_deeply( [ $fseq->get ], [ default => "handler" ], + '->catch accepts a default handler' ); +} + +# catch via 'then' +{ + is( scalar ( Future->fail( "message", test => ) + ->then( sub { die "then &done should not be invoked" }, + test => sub { Future->done( 1234 ) }, + sub { die "then &fail should not be invoked" } )->get ), + 1234, 'catch semantics via ->then' ); +} + +done_testing;
From: Jakub Narębski <jnareb [...] gmail.com>
Some (if not all) JavaScript libraries implementing promises also implement .catch(function(err) { ... }), which I found via http://eddywashere.com/blog/switching-out-callbacks-with-promises-in-mongoose/