Initial version
Yuval Kogman [Mon, 31 Aug 2009 18:55:53 +0000 (21:55 +0300)]
MANIFEST.SKIP [new file with mode: 0644]
Makefile.PL [new file with mode: 0644]
lib/Try/Tiny.pm [new file with mode: 0644]
t/basic.t [new file with mode: 0644]
t/when.t [new file with mode: 0644]

diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP
new file mode 100644 (file)
index 0000000..c839e85
--- /dev/null
@@ -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 (file)
index 0000000..af62298
--- /dev/null
@@ -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 (file)
index 0000000..53ffa59
--- /dev/null
@@ -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<try>/C<catch> 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<TryCatch> which provides a nice syntax and avoids adding
+another call stack layer, and supports calling C<return> from the try block to
+return from the parent subroutine. These extra features come at a cost of a few
+dependencies, namely L<Devel::Declare> and L<Scope::Upper> which are
+occasionally problematic, and the additional catch filtering using L<Moose>
+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<TryCatch>, but who still want to
+write correct C<eval> 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<BACKGROUND>) 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<Exporter>.
+
+In the future L<Sub::ExporteR> 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<eval> 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<try>.
+
+=back
+
+=head1 BACKGROUND
+
+There are a number of issues with C<eval>.
+
+=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<eval> in order to avoid this issue.
+
+=head2 Localizing $@ silently masks errors
+
+Inside an eval block C<die> 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<Object::DESTROY> is not localizing C<$@> but using eval it
+will set C<$@> to C<"">.
+
+The destructor is only fired after C<die> sets C<$@> to
+C<"foo at Foo.pm line 42\n">, so by the time C<if ( $@ )> 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<eval> in destructors, we
+can at least be sure there was an error:
+
+       my $failed = not eval {
+               ...
+
+               return 1;
+       };
+
+This is because an C<eval> that caught a C<die> will always behave like
+C<return> with no arguments.
+
+=head1 SHINY SUGAR
+
+Using Perl 5.10 you can enable the C<given>/C<when> construct. The C<catch>
+block is invoked in a topicalizer context (like a C<given> block).
+
+Note that you can't return a useful value from C<catch> using the C<when>
+blocks.
+
+This is somewhat similar to Perl 6's C<CATCH> 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<Sub::Uplevel> is not used. L<Carp>
+will report this when using full stack traces. This is considered a feature.
+
+=item *
+
+The value of C<$_> in the C<catch> block is not guaranteed to be preserved,
+there is no safe way to ensure this if C<eval> is used unhygenically in
+destructors. It is guaranteed that C<catch> will be called, though.
+
+=back
+
+=head1 SEE ALSO
+
+=over 4
+
+=item L<TryCatch>
+
+Much more feature complete, more convenient semantics, but at the cost of
+implementation complexity.
+
+=item L<Error>
+
+Exception object implementation with a C<try> statement. Does not localize
+C<$@>.
+
+=item L<Exception::Class::TryCatch>
+
+Provides a C<catch> statement, but properly calling C<eval> is your
+responsibility.
+
+The C<try> 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<http://github.com/nothingmuch/try-tiny/>
+
+=head1 AUTHOR
+
+Yuval Kogman E<lt>nothingmuch@woobling.orgE<gt>
+
+=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 (file)
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 (file)
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" );