Beefy Boxes and Bandwidth Generously Provided by pair Networks
Keep It Simple, Stupid
 
PerlMonks  

Error handling in chained method calls

by szabgab (Priest)
on Oct 23, 2010 at 15:11 UTC ( [id://866961]=perlquestion: print w/replies, xml ) Need Help??

szabgab has asked for the wisdom of the Perl Monks concerning the following question:

If I want to call
$thing->do_something
in order to make sure I can really call it I have to do something like
if (definde $thing and ref($thing) and $thing->can('do_something')) { $thing->do_something }
or I need to wrap the method call in an evel block.

When trying to do chained method calls such as

$thing->get_object->get_another_object->do_something
Well, in that case a lot of things can go wrong and return undef or anything else that cannot handle the (interim) method calls.

I wonder how do you deal with such situations?

Replies are listed 'Best First'.
Re: Error handling in chained method calls
by moritz (Cardinal) on Oct 23, 2010 at 15:55 UTC
    I wonder how do you deal with such situations?

    The question is really what should happen if one of those steps fail?

    If the answer is "throw an error", then I don't do anything. If the answer is "don't throw an error, and just continue normally", I wrap it all in an eval block.

    Perl 6 - links to (nearly) everything that is Perl 6.
Re: Error handling in chained method calls
by shevek (Beadle) on Oct 23, 2010 at 17:10 UTC
    I really hope I am not opening a flaming can of worms here, but how about don't chain methods at all? If you don't really need to do it that way and can call each method and check the return values before continuing then don't chain methods-- I think that would be preferred from a maintenance and clarity of code for your average programmer viewpoint. It may cost you more typing, but it is very helpful if you are stepping through the code to be able to put a breakpoint on any of those method calls as well as trace into them.

      I don’t think you run the risk of cooking any night-crawlers here.   These days, the only thing that really matters is that the code you write must be serviceable and clear.   “Cleverness” is not rewarded.   If “chained method calls” seem to be getting in the way of “error handling” (and whether you are attempting them in “just the right way” or not), just scrap that approach and do something that is more obvious and clear ... to you and to those who will follow you.

Re: Error handling in chained method calls
by phaylon (Curate) on Oct 23, 2010 at 17:43 UTC

    Have a look at this article by Matt S Trout. The 'maybe' routine is basically one of the ways I usually do this.


    Ordinary morality is for ordinary people. -- Aleister Crowley
Re: Error handling in chained method calls
by BrowserUk (Patriarch) on Oct 24, 2010 at 00:47 UTC

    Under what circumstances are you likely to get an error from:$thing->do_something;?

    1. $thing could be undef.

      If you create $thing, and you error checked that creation my $thing = Some::Class->new(...) or die;, this can't happen.

      If your code is passed $thing and you validated your inputs:

      sub SomeSubOrMethod { my $thing = shift; croak 'Bad param' unless $thing and ref $thing; ...

      This can't happen.

    2. $thing might be a none ref, or non-blessed ref.

      If new() or similar constructor returns a non-ref or non-blessed ref, it is a fundamental coding error that will be discovered the first time class is used. Even the simplest of tests, during the development of the bad class, or when you first try to use that class will detect this. It couldn't possibly make it into production unless you are in the habit of putting code that has never even been run into production.

    3. $thing is of a class that doesn't have a do_something method; or equivalently, do_something is typed wrong.

      If you created $thing yourself, then you know what class it is, so you'll know what methods it has. Your testing should be sufficient to detect any such typos long before they become a production problem.

      If you are passed $thing, and you've validated your inputs, then you've ensured that it is of a type that you can reasonably expect to have a do_something method.

    In short. DON'T perform such checks. Allow them to fail immediately and loudly.

    This will ensure that if the circumstances arise, the clearest possible indication of what has gone wrong is given to the programmer who a) typoed the method name (usually you); or b) passed your code a non-reference, or reference to the wrong class of object.

    The alternatives are:

    • You detect the error and issue an alternative error message.

      Can you give a better error message than croaking with trace back?

    • You detect the error and do what?

      Call a different method? Pretend you didn't need to call that method?

    • Other?

    In the end, even if you perform your blow by blow validation every time before you call a method--besides the horrible performance penalty you impose--there is no guarantee that the do_something method:

    • does the something you are expecting it to do;
    • or accepts the arguments you are going to pass;
    • or returns the result you are expecting;

    In vast majority of cases, the method will exist and do what is required. In the remaining percentage of cases, there's nothing you can reasonably do about it. So, just get on with it and let it fail on those rare occasions when it will.


    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
      > besides the horrible performance penalty you impose

      Not necessarily.

      If the methods are immutable and follow the "cascading convention" to always return $self it should be possible to be much faster by memoizing name and reference of allowed methods in a hash.

      The trick would be to use the $obj->$methref() syntax, compare

      Re: Error handling in chained method calls

      IIRC normal method calls are ten times slower than a function call.

      So if performance matters this could be a legitimate way to boost "alien" classes one isn't allowed to change.

      Cheers Rolf

        Hm. I'm pretty sure that Perl already does method caching, so adding your own would be redundant.

        But mostly, why? There's no good reason to test this, as there's nothing better you can do that fail anyway. So what would be the point of adding all the complexity, when all you can do if you do detect a problem is fail anyway. Its just pointless.


        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.
      Well, $node->parent can either return another node, or undef if the node has no parent (e.g. it is a root of a tree). Thus, $node->parent->parent can sometimes work and sometimes not. The problem is the chaining.
        Thanx for this clarification of a use case. :)

        IMHO again this could be solved with a coderef wrapper $c_parent which calls the real method only on definedness of the object.

         $node->$c_parent->$c_parent

        Another (classical) approach would be defining a NULL-node object which is returned instead of undef and wouldn't cause an error.

        But the latter would only work if one has access to the design of underlying library.

        Cheers Rolf

        Thus, $node->parent->parent can sometimes work and sometimes not.

        Then the situation is flow control logic rather than error handling.

        If the node doesn't have a parent, you can't get it's grandparent, and so there is no logical option but to split the statement into parts. That isn't really a case of the chaining being the problem so much as simply a bad algorithm.

        Taking it back to the OPs example, if you're chaining your way back to the root, you have to check whether you've reached there or not at each iteration, but if you've successfully retrieved a defined something from if( defined obj->parent ), you don't need to then check that it is a reference, or whether that reference can( 'parent' ), because the parent method should not be able to return anything other than a valid node, or undef.

        A simple:

        sub root { my $self = shift; my $root = $self; $root = $_ while defined( $_ = $root->parent ); $root //= $self; return $root; }

        Or, if you're dealing with an algorithm that requires access to grandparent nodes, you have to deal with nodes that don't have a parent never mind a grandparent:

        sub grandparent { my $self = shift; my $parent = $self->parent // return; return $parent->parent; } ... if( my $gp = $obj->grandparent ) { ## use it; } else{ ## take a different path }

        But you don't (shouldn't) have to deal with getting a defined something that isn't a node.


        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.
Re: Error handling in chained method calls
by LanX (Saint) on Oct 23, 2010 at 19:02 UTC
    Maybe a wrapper method $safecall is what you need, you can put all runtime checks needed in there.

    Please note that arguments are optional and need to be passed as array_refs in order to be distinguished from methodnames.

    $\="\n"; sub safecall{ db(@_); my $obj=shift; # TODO check object/class my ($meth,$a_args); while (@_){ $meth=shift; # TODO check method $a_args= (ref $_[0] eq "ARRAY" ? shift : []); $obj=$obj->$meth(@$a_args); # TODO check ret +urned obj } print "-"x20; return $obj; }; my $safecall=\&safecall; my $obj={}; bless $obj, CLASS; #- USAGE/INTERFACE - $obj->$safecall( "meth1" ); $obj->$safecall( meth1=> meth2=> [3,4,5] ); $obj->$safecall( qw(meth1 meth2) ); $obj->$safecall( meth1=>[1,2,3] , meth2=> [3,4,5] ); $obj->$safecall( meth1=>[1,2,3] ) -> meth2(3,4,5); # still possible safecall($obj, meth1=>[1,2,3] , meth2=> [3,4,5]); # fuctional appr +oach package CLASS; sub meth1 { my $self=shift; print "call meth1 with (@_)"; return $self; } sub meth2 { my $self=shift; print "call meth2 with (@_)"; return $self; }

    OUTPUT:

    call meth1 with () ---------- call meth1 with () call meth2 with (3 4 5) ---------- call meth1 with () call meth2 with () ---------- call meth1 with (1 2 3) call meth2 with (3 4 5) ---------- call meth1 with (1 2 3) ---------- call meth2 with (3 4 5)

    Cheers Rolf

    UPDATE: added functional approach.

      Hm ...

      I'm not sure if it's worth it, but by applying Attribute::Handlers on safecall it could even be possible to run some checks at compile time.

      Maybe its better to rename $safecall to $CHAIN or $_chain to make the syntax "sweeter" and make conflicts unlikely if the $ is missing.

      $obj->$CHAIN( meth1=>[1,2,3] , meth2=>[3,4,5] );

      Cheers Rolf

      UPDATE: it should even be possible to add a method safecall to UNIVERSAL, to avoid always putting the $ in front, but I'm a little reluctant because of possible side effects.

Re: Error handling in chained method calls
by ikegami (Patriarch) on Oct 24, 2010 at 08:39 UTC
    my $foo = $self->get_foo() or die; my $bar = $foo->get_bar() or die;
Re: Error handling in chained method calls
by LanX (Saint) on Oct 23, 2010 at 17:28 UTC
    > in order to make sure I can really call it I have to do something like

    I'm not sure if it covers your question but perl supports a coderef syntax for method calls (which BTW is much faster)

    $thing->$c_get_object->$c_get_another_object->$c_do_something

    (ATM I can't find the corresponding perldoc) ¹

    So when your sure that the methods are not dynamically added or deleted at run-time from the corresponding class, you can make sure that that those methods exist and init the coderefs like constants (e.g. in a BEGIN block).

    Cheers Rolf

    UPDATE: 1) greping helps, see perltooc#Inheritance Concerns:

    Because the &UNIVERSAL::can method returns a reference to the function directly, you can use this directly for a significant performance improvement:
    for my $parent (@ISA) { if (my $coderef = $self->can($parent . "::CData1")) { $self->$coderef($newvalue); } }
Re: Error handling in chained method calls
by LanX (Saint) on Oct 23, 2010 at 16:42 UTC
    I'm not sure which problem you have...

    Is the error message of a failed interim method call not clear enough?

    Or do you wanna have a compile time check?

    Cheers Rolf

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://866961]
Approved by Old_Gray_Bear
Front-paged by LanX
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others studying the Monastery: (3)
As of 2024-04-16 18:43 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found