Subject: Re: Git.pm

From: Andrew Sayers <[email-masked]>
Date: Thu, 10 May 2012 21:55:10 +0100

Try::Tiny is an increasingly standard part of Perl - for example, it's
used extensively in Moose.  There's a good list of arguments about why
you should use it instead of eval in the Try::Tiny documentation:
http://search.cpan.org/~nuffin/Try-Tiny-0.01/lib/Try/Tiny.pm

Now I've got that talking point done, here's what I really think :)

Try::Tiny is designed on the assumption that throwing and catching
objects is something people should do all the time, and it can cause
subtle errors that are only worth the hassle if you get a lot of benefit
from doing so.  It's easy enough to come up with ideas for where they
might be useful, but in the real world advanced uses for exceptions are
usually a sign you're doing it wrong.  Three of the most common reasons
for frequent/complex exceptions are handling errors further up the call
stack, recovering from operations that fail, and clever error-handling.

If you want exceptions to be caught by code further up the call stack
than the immediate caller, you're likely to be disappointed.  This is
one of the places where "separation of concerns" applies - if I use a
module that uses a module that uses your module, then catching
exceptions from your code will just cause my program to break when some
module in the middle obscures your error by adding its own layer of
error handling.

If you have an operation that really might fail, and you want to
encourage most people to handle it most of the time, it's better to have
a function with a meaningful name and good documentation.  This puts the
burden on the calling function to handle the error instead of letting
them think "oh well, if it dies someone else will handle it".  It also
forces you to split functions along boundaries that make your code
readable, instead of falling for the temptation to make something that
"just works"... until it doesn't, and the maintainer has to go
spelunking through code they don't know.  So instead of:

   try {
       Foo::frobnicate( widgets => 3 );
   } catch {
      if (ref($_) eq 'Error::Widget') {
          die "Could not add 3 widgets";
      }
   }

It's better to ask the people using your module to write:

   my $foo = Foo->new;
   $foo->add_widgets(3) or die "could not add 3 widgets"
   $foo->frobnicate;

This is easier to document, easier to write and easier to read.

If you have an operation where calling code is supposed to do something
more complicated than give up, it's better to use a callback.  This
gives you an opportunity to document what's needed, and to check that
the calling code is doing the right thing before it's too late.  So
instead of:

    my $widgets = 3;
    while ( $widgets ) {

        try {
            Foo::frobnicate($widgets);
            $widgets = 0;
        } catch {
            if ( $_->{remaining_widgets} < 2 ) {
                die $_->{error};
            } elsif ( $_->{remaining_widgets} == 2 ) {
                $widgets = 0;
            }
        }

    }

It's better to ask people using your module to write:

    Foo::Frobnicate(
        widgets => 3,
        error_handler => sub {
             my ( $remaining_widgets, $error ) = @_;
             die $error if $broken_widgets < 2;
             return "give up" if $remaining_widgets == 2;
             return "continue";
        },
    );

Again, this is more readable and easier to document.

Aside from the philosophical angle, Try::Tiny is particularly hard to
maintain because it looks like a language extension, but is actually
just an ordinary module.  The try {} and catch {} blocks are anonymous
subroutines, which lead to some wonderfully unintuitive behaviour.  See
what you think these do, then run the code to find out:

    sub foo {
        try {
            return 1;
        }
        return 0;
    }

    sub bar {

        our @args = @_;
        our @ret;

        try {
            @ret =
                wantarray
                    ?   grep( /blah/, @args )
                    : [ grep( /blah/, @args ) ]
        };

        return @ret;
    }

    my $foo = "bar";
    sub baz {
        my $foo = "baz";
        try {
            print $foo;
        }
    }

    sub qux {
        my $ret;
        try {
            $ret = "value";
        }
        return $ret;
    }

    print foo, "\n";
    print bar( "blah", "blip" ), "\n";
    baz;
    print qux, "\n";

In short, Try::Tiny looks like a lot of gain for not much pain, but
actually it's the other way around.

	- Andrew