From: Yuval Kogman Date: Mon, 31 Aug 2009 18:55:53 +0000 (+0300) Subject: Initial version X-Git-Tag: Try-Tiny-0.01~5 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=3176feef3e350892f94014e6ca02bf8e45115ea5;p=p5sagit%2FTry-Tiny.git Initial version --- 3176feef3e350892f94014e6ca02bf8e45115ea5 diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP new file mode 100644 index 0000000..c839e85 --- /dev/null +++ b/MANIFEST.SKIP @@ -0,0 +1,46 @@ +# Avoid version control files. +\bRCS\b +\bCVS\b +\bSCCS\b +,v$ +\B\.svn\b +\B\.git\b +\b_darcs\b + +# Avoid Makemaker generated and utility files. +\bMANIFEST\.bak +\bMakefile$ +\bblib/ +\bMakeMaker-\d +\bpm_to_blib\.ts$ +\bpm_to_blib$ +\bblibdirs\.ts$ # 6.18 through 6.25 generated this + +# Avoid Module::Build generated and utility files. +\bBuild$ +\b_build/ + +# Avoid temp and backup files. +~$ +\.old$ +\#$ +\b\.# +\.bak$ + +# Avoid Devel::Cover files. +\bcover_db\b + +### DEFAULT MANIFEST.SKIP ENDS HERE #### + +\.DS_Store$ +\.sw.$ +(\w+-)*(\w+)-\d\.\d+(?:\.tar\.gz)?$ + +\.t\.log$ + +\.prove$ + +# XS shit +\.(?:bs|c|o)$ + +\.gitignore$ diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..af62298 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,14 @@ +#!/usr/bin/perl -w + +use strict; + +use ExtUtils::MakeMaker; + +WriteMakefile( + NAME => 'Try::Tiny', + VERSION_FROM => 'lib/Try/Tiny.pm', + INSTALLDIRS => 'site', + SIGN => 1, + PL_FILES => { }, + PREREQ_PM => { }, +); diff --git a/lib/Try/Tiny.pm b/lib/Try/Tiny.pm new file mode 100644 index 0000000..53ffa59 --- /dev/null +++ b/lib/Try/Tiny.pm @@ -0,0 +1,317 @@ +package Try::Tiny; + +use strict; +use warnings; + +use base qw(Exporter); +use vars qw(@EXPORT @EXPORT_OK $VERSION); + +$VERSION = "0.01"; + +$VERSION = eval $VERSION; + +@EXPORT = @EXPORT_OK = qw(try catch); + +sub try (&;$) { + my ( $try, $catch ) = @_; + + # we need to save this here, the eval block will be in scalar context due + # to $failed + my $wantarray = wantarray; + + my ( @ret, $error, $failed ); + + # FIXME consider using local $SIG{__DIE__} to accumilate all errors. It's + # not perfect, but we could provide a list of additional errors for + # $catch->(); + + { + # localize $@ to prevent clobbering of previous value by a successful + # eval. + local $@; + + # failed will be true if the eval dies, because 1 will not be returned + # from the eval body + $failed = not eval { + + # evaluate the try block in the correct context + if ( $wantarray ) { + @ret = $try->(); + } elsif ( defined $wantarray ) { + $ret[0] = $try->(); + } else { + $try->(); + }; + + return 1; # properly set $fail to false + }; + + # copy $@ to $error, when we leave this scope local $@ will revert $@ + # back to its previous value + $error = $@; + } + + # at this point $failed contains a true value if the eval died even if some + # destructor overwrite $@ as the eval was unwinding. + if ( $failed ) { + # if we got an error, invoke the catch block. + if ( $catch ) { + # This works like given($error), but is backwards compatible and + # sets $_ in the dynamic scope for the body of C<$catch> + for ($error) { + return $catch->($error); + } + } else { + return; + } + } else { + # no failure, $@ is back to what it was, everything is fine + return $wantarray ? @ret : $ret[0]; + } +} + +sub catch (&) { + return $_[0]; +} + + +__PACKAGE__ + +__END__ + +=pod + +=head1 NAME + +Try::Tiny - minimal try/catch with proper localization of $@ + +=head1 SYNOPSIS + + # handle errors with a catch handler + try { + die "foo"; + } catch { + warn "caught error: $_"; + }; + + # just silence errors + try { + die "foo"; + }; + +=head1 DESCRIPTION + +This module provides bare bones C/C statements that are designed to +minimize common mistakes done with eval blocks (for instance assuming that +C<$@> is set to a true value on error, or clobbering previous values of C<$@>), +and NOTHING else. + +This is unlike L which provides a nice syntax and avoids adding +another call stack layer, and supports calling C from the try block to +return from the parent subroutine. These extra features come at a cost of a few +dependencies, namely L and L which are +occasionally problematic, and the additional catch filtering using L +type constraints may not be desirable either. + +The main focus of this module is to provide reliable but simple error handling +for those having a hard time installing L, but who still want to +write correct C blocks without 5 lines of boilerplate each time. + +It's designed to work as correctly as possible in light of the various +pathological edge cases (see L) and to be compatible with any style +of error values (simple strings, references, objects, overloaded objects, etc). + +=head1 EXPORTS + +All are exported by default using L. + +In the future L may be used to allow the keywords to be renamed, +but this technically does not satisfy Adam Kennedy's definition of "Tiny". + +=over 4 + +=item try &;$ + +Takes one mandatory and one optional catch subroutine. + +The mandatory subroutine is evaluated in the context of an C block. + +If no error occured the value from the first block is returned. + +If there was an error and the second subroutine was given it will be invoked +with the error in C<$_> (localized) and as that block's first and only +argument. + +Note that the error may be false + +=item catch & + +Just retuns the subroutine it was given. + + catch { ... } + +is the same as + + sub { ... } + +Intended to be used in the second argument position of C. + +=back + +=head1 BACKGROUND + +There are a number of issues with C. + +=head2 Clobbering $@ + +When you run an eval block and it succeeds, C<$@> will be cleared, potentially +cloberring an error that is currently being caught. + +C<$@> must be properly localized before invoking C in order to avoid this issue. + +=head2 Localizing $@ silently masks errors + +Inside an eval block C behaves sort of like: + + sub die { + $@_ = $_[0]; + return_undef_from_eval(); + } + +This means that if you were polite and localized C<$@> you can't die in that +scope while propagating your error. + +The workaround is very ugly: + + my $error = do { + local $@; + eval { ... }; + $@; + }; + + ... + die $error; + +=head2 $@ might not be a true value + +This code is wrong: + + if ( $@ ) { + ... + } + +because due to the previous caveats it may have been unset. $@ could also an +overloaded error object that evaluates to false, but that's asking for trouble +anyway. + +The classic failure mode is: + + sub Object::DESTROY { + eval { ... } + } + + eval { + my $obj = Object->new; + + die "foo"; + }; + + if ( $@ ) { + + } + +In this case since C is not localizing C<$@> but using eval it +will set C<$@> to C<"">. + +The destructor is only fired after C sets C<$@> to +C<"foo at Foo.pm line 42\n">, so by the time C is evaluated it has +become false. + +The workaround for this is even uglier. Even though we can't save the value of +C<$@> from code that doesn't localize it but uses C in destructors, we +can at least be sure there was an error: + + my $failed = not eval { + ... + + return 1; + }; + +This is because an C that caught a C will always behave like +C with no arguments. + +=head1 SHINY SUGAR + +Using Perl 5.10 you can enable the C/C construct. The C +block is invoked in a topicalizer context (like a C block). + +Note that you can't return a useful value from C using the C +blocks. + +This is somewhat similar to Perl 6's C blocks. You can use it to +concisely match errors: + + try { + require Foo; + } catch { + when (qr/^Can't locate .*?\.pm in \@INC/) { } # ignore + default { die $_ } + } + +=head1 CAVEATS + +=over 4 + +=item * + +Introduces another caller stack frame. L is not used. L +will report this when using full stack traces. This is considered a feature. + +=item * + +The value of C<$_> in the C block is not guaranteed to be preserved, +there is no safe way to ensure this if C is used unhygenically in +destructors. It is guaranteed that C will be called, though. + +=back + +=head1 SEE ALSO + +=over 4 + +=item L + +Much more feature complete, more convenient semantics, but at the cost of +implementation complexity. + +=item L + +Exception object implementation with a C statement. Does not localize +C<$@>. + +=item L + +Provides a C statement, but properly calling C is your +responsibility. + +The C keyword pushes C<$@> onto an error stack, avoiding some of the +issues with C<$@> but you still need to localize to prevent clobbering. + +=back + +=head1 VERSION CONTROL + +L + +=head1 AUTHOR + +Yuval Kogman Enothingmuch@woobling.orgE + +=head1 COPYRIGHT + + Copyright (c) 2009 Yuval Kogman. All rights reserved. + This program is free software; you can redistribute + it and/or modify it under the terms of the MIT license. + +=cut + diff --git a/t/basic.t b/t/basic.t new file mode 100644 index 0000000..8182137 --- /dev/null +++ b/t/basic.t @@ -0,0 +1,97 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More tests => 15; + +BEGIN { use_ok 'Try::Tiny' }; + +sub _eval { + local $@; + local $Test::Builder::Level = $Test::Builder::Level + 2; + return ( scalar(eval { $_[0]->(); 1 }), $@ ); +} + + +sub lives_ok (&$) { + my ( $code, $desc ) = @_; + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my ( $ok, $error ) = _eval($code); + + ok($ok, $desc ); + + diag "error: $@" unless $ok; +} + +sub throws_ok (&$$) { + my ( $code, $regex, $desc ) = @_; + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my ( $ok, $error ) = _eval($code); + + if ( $ok ) { + fail($desc); + } else { + like($error || '', $regex, $desc ); + } +} + + +sub Evil::DESTROY { + eval { "oh noes" }; +} + +lives_ok { + try { + die "foo"; + }; +} "basic try"; + +throws_ok { + try { + die "foo"; + } catch { die $_ }; +} qr/foo/, "rethrow"; + + +{ + local $@ = "magic"; + is( try { 42 }, 42, "try block evaluated" ); + is( $@, "magic", '$@ untouched' ); +} + +{ + local $@ = "magic"; + is( try { die "foo" }, undef, "try block died" ); + is( $@, "magic", '$@ untouched' ); +} + +{ + local $@ = "magic"; + like( (try { die "foo" } catch { $_ }), qr/foo/, "catch block evaluated" ); + is( $@, "magic", '$@ untouched' ); +} + +is( scalar(try { qw(foo bar gorch) }), "gorch", "scalar context" ); +is_deeply( [ try {qw(foo bar gorch)} ], [qw(foo bar gorch)], "list context" ); + + + +{ + local $@ = "magic"; + local $_ = "other magic"; + + try { + my $object = bless { }, "Evil"; + die "foo"; + } catch { + pass("catch invoked"); + local $TODO = "i don't think we can ever make this work sanely, maybe with SIG{__DIE__}"; + like($_, qr/foo/); + }; + + is( $@, "magic", '$@ untouched' ); + is( $_, "other magic", '$_ untouched' ); +} diff --git a/t/when.t b/t/when.t new file mode 100644 index 0000000..a411ac5 --- /dev/null +++ b/t/when.t @@ -0,0 +1,37 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More; + +BEGIN { + plan skip_all => "Perl 5.10 required" unless eval { require 5.010; 1 }; + plan tests => 6; +} + + +BEGIN { use_ok 'Try::Tiny' } + +use 5.010; + +my ( $foo, $bar, $other ); + +$_ = "magic"; + +try { + die "foo"; +} catch { + + like( $_, qr/foo/ ); + + when (/bar/) { $bar++ }; + when (/foo/) { $foo++ }; + default { $other++ }; +}; + +is( $_, "magic", '$_ not clobbered' ); + +ok( !$bar, "bar didn't match" ); +ok( $foo, "foo matched" ); +ok( !$other, "fallback didn't match" );