Beefy Boxes and Bandwidth Generously Provided by pair Networks
P is for Practical
 
PerlMonks  

Unlimited chaining (is there a way to detect this?)

by Burak (Chaplain)
on Jan 05, 2008 at 22:43 UTC ( #660583=perlquestion: print w/ replies, xml ) Need Help??
Burak has asked for the wisdom of the Perl Monks concerning the following question:

I just noticed that if a base class returns an instance of it's child from a method, then you can write this dumb code:
print $o->bar->bar->bar->bar->bar->bar->bar->bar->bar->bar->bar->bar-> +bar->bar->baz;
My questions are:
  1. Is there a technical term for this thing (other than "method chaining" of course!)?
  2. Is it possible to detect it? (caller does not seem to give any info for this)
Full code:
#!/usr/bin/perl -w use strict; package BaseClass; use strict; sub new { my $class = shift; my $self = {}; bless $self, $class; $self; } sub bar { return Foo->new; } package Foo; use strict; use base qw(BaseClass); sub baz {"Some data"} package main; use strict; my $o = Foo->new; print $o->bar->bar->bar->bar->bar->bar->bar->bar->bar->bar->bar->bar-> +bar->bar->baz;

Comment on Unlimited chaining (is there a way to detect this?)
Select or Download Code
Re: Unlimited chaining (is there a way to detect this?)
by rafl (Friar) on Jan 05, 2008 at 23:48 UTC

    You could store a flag in each object created by the bar method and check that in bar.

    #!/usr/bin/perl -w use strict; package BaseClass; use strict; sub new { my $class = shift; my $self = {}; bless $self, $class; $self; } sub bar { return Foo->new; } package Foo; use strict; use Carp; use base qw(BaseClass); sub baz {"Some data"} sub bar { my $self = shift; warn "bar"; croak "don't chain method calls" if $self->{chained}; my $ret = $self->SUPER::bar(@_); $ret->{chained} = 1; return $ret; } package main; use strict; my $o = Foo->new; print $o->bar->bar->bar->bar->bar->bar->bar->bar->bar->bar->bar->bar-> +bar->bar->baz; __END__ bar at -e line 1. bar at -e line 1. don't chain method calls to bar at -e line 0

    Unfortunately that breaks encapsulation, but maybe your baseclass provides an api to do it cleanly.

      Yes can be. But this does not work if new() uses a scalar (which is what I did after realising that I don't really use the underlying hash):
      sub new { bless \do{my $anon_scalar}, shift }
        Why not? No one forces you to store the flags (in this case) in the object itself.

        Open source softwares? Share and enjoy. Make profit from them if you can. Yet, share and enjoy!

Re: Unlimited chaining (is there a way to detect this?)
by merlyn (Sage) on Jan 06, 2008 at 01:44 UTC
    First, the idea that an instance method might actually return a different instance of the same class is actually useful. For example, I use this technique in my File::Finder.

    Second, what do you mean 'detect it'? To me, your question reads like: "Oh, when I loop like this, it loops forever! How do I stop it:"

    while (1) { ... }
    The way to stop it is to stop doing it. You're the programmer. You're in charge. Don't do stupid things.

      I'm not sure you can say that.

      If he provides a module for others to use, he's not necessarily the one doing the weird looping call and so he might want to detect it. So the question is valid, from my perspective. What's the best method for detecting this looped method calling?

      --
      I used to drive a Heisenbergmobile, but every time I looked at the speedometer, I got lost.
        What's the best method for detecting this looped method calling?

        Code reviews.

      Second, what do you mean 'detect it'?
      Well... To be able to display a warning (or die) like: "Deep recursion on method foo ... " after a certain count. If that makes sense? It was just a thought :) actually re-designing the code like this solves this issue (yes, I've stopped doing it):
      package NewBase; use strict; sub new { my $class = shift; my $self = {}; bless $self, $class; $self; } # other methods package BaseClass; use strict; use base qw(NewBase); sub bar { return Foo->new; } package Foo; use strict; use base qw(NewBase);
      However, the situation about this method chaining seemed interesting to me and I opened a discussion about it.
Re: Unlimited chaining (is there a way to detect this?)
by ysth (Canon) on Jan 06, 2008 at 02:32 UTC
      hmmm... I didn't get how will want('OBJECT') will help since bar() only returns an object...
        By "Is it possible to detect it?" I was assuming you meant detect when the return from the current method will be the object of a method call. That's what want('OBJECT') does. From your other replies, it seems what you are looking for is somehow to detect when a method is called on its previous return "too many" times. I'm not sure what the problem calling code would look like, though.
Re: Unlimited chaining (is there a way to detect this?) (Foo::bar)
by tye (Cardinal) on Jan 06, 2008 at 02:52 UTC

    I too am unsure why you feel the need to detect / prevent this. But the first technique for doing that which pops into my head has not been mentioned yet:

    sub Foo::bar { croak "bar() only meant for use from base class" }

    or thereabouts. Of course, you've taken all of the meaning out of your class and method names so it is hard to "get" what your point is in such an abstract case.

    - tye        

Re: Unlimited chaining (is there a way to detect this?)
by dragonchild (Archbishop) on Jan 06, 2008 at 03:09 UTC
    In other words, you've set up a situation where $o->bar returns $o2 and blessed($o) eq $blessed($o2). So, it's mathematically provable that it literally doesn't matter* how many calls to bar() you make before the call to baz(). Furthermore, you haven't made a case as to why the author of Foo and BaseClass even cares whether or not baz() is called once or 1.233212e1234 times. So, don't worry about it until you have a reason to worry about it.

    *: Well, it does matter depending on how well the VM detects that the intermediate objects can be thrown away immediately. Otherwise, you can theoretically get into a situation where you run out of RAM after enough chained calls. But, that number of chained calls would be so ridiculously high that you're just as likely to get into a problem with running out of RAM during parsing or just storing the file on disk. (Have you ever tried running a 500M .pm file?)


    My criteria for good software:
    1. Does it work?
    2. Can someone else come in, make a change, and be reasonably certain no bugs were introduced?
Re: Unlimited chaining (is there a way to detect this?)
by perrin (Chancellor) on Jan 06, 2008 at 03:10 UTC
    I hate this style of code too. I blame SOAP::Lite for it, but I think it was actually imported from another language. How can you prevent it? Don't return an object instance from methods that are not intended to create objects!

      I really dislike the SOAP::Lite style of chained methods constructors

      my $soap_client = SOAP::Lite->proxy($proxy) ->uri($uri) ->on_fault(sub {...}); .... my $soap_value = SOAP::Data ->name($name) ->value($value) ->type($type);

      and don't understand why is it pursued overall in the documentation at the expense of ordinary new:

      my $soap_value = SOAP::Data->new( 'name' => $name, 'value' => $value, 'type' => $type);

      But I think the SOAP::Lite style is different from the one discussed here.

      AFAIK SOAP::Lite methods are overloaded as being:

      • setters (when called on object with parameter)
      • constructors (when called on package with parameter). They even can be imported sometimes as functions.
      • getters (called without parameters)

      but only the setter variant can be chained as it modifies the current object and returns it. So there are no new objects created by chained methods.

Re: Unlimited chaining (is there a way to detect this?)
by weierophinney (Pilgrim) on Jan 06, 2008 at 20:11 UTC
    Martin Fowler calls this a "fluent interface": http://martinfowler.com/bliki/FluentInterface.html

    The idea behind it is that methods that are simply setting object state return the object instance; this can improve readability in many instances, particularly if you align the '->' pointers vertically, and can be used to build constructs that have a natural linguistic flow -- hardly 'dumb' code.

    As for detecting it... simply don't write such constructs; the only way you'd be able to programmatically generate such a construct is to pass it to eval, and eval is rarely the right answer.
      Interesting... I didn't hear that "fluent interface" before :)
Re: Unlimited chaining (is there a way to detect this?)
by hipowls (Curate) on Jan 07, 2008 at 12:54 UTC

    This thread has provoked much thought my part and I would like to hear other monks' opinions

    package Person; sub init { ... $self->set_title($title) ->set_first_name($first) ->set_last_name($last); ... }
    Here you don't really care which setter blew up, the means to repair the damage lies further up the call stack where the caller of $person->init() is prepared to deal with Person errors. It is perfectly reasonable to chain methods.

    On the otherhand

    sub mail_customers { foreach my $customer (@customers) { ... my $mail_address = $customer->get_address->as_string; ... } }
    is more problematic. Can you be certain that every customer has a valid address object? What about the customers with the special nil address object that stringifies as "Whereabouts unknown"? Is the caller of mail_customers prepared to handle address exceptions? Should mail_customer handle them so it can process the valid customers and then how does the caller find out about the invalid ones?

    OK you have validated the input and know that can't happen. What about when mail_customers is extended to keep stats?

    $stats{ $customer->address->postcode }++; # zip codes
    Does the Solomon Islands have postcodes? I don't know and I suspect that most won't. The problem arises because of the distance between validation of data and its use.

    Perhaps mail_customers should only process one customer at a time (and be renamed to mail_customer) and have the loop outside

    foreach my $customer(@customers) { next if ! valid_mail_recipient($customer); eval { mail_customer($customer); }; if ($@) { ... } }
    but then the loop goes around every call to mail_customer instead of being factored out to mail_customers, however we put the validation of the data closer to where it is used. Maybe that eval block can go. Swings and roundabouts;

    My conclusion is that chained methods on the same object for related purposes is a valid idiom which make code easier to understand. Chaining methods on different objects or unrelated methods on the same object, e.g. $reactor->check_pressure->raise_rods, can be dangerous and need to be used with caution. Chaining ten levels is probably ridiculous but with a good example I could be persuaded it is OK in that particular circumstance.

Log In?
Username:
Password:

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

How do I use this? | Other CB clients
Other Users?
Others musing on the Monastery: (7)
As of 2014-10-22 03:21 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    For retirement, I am banking on:










    Results (112 votes), past polls