Beefy Boxes and Bandwidth Generously Provided by pair Networks
Don't ask to ask, just ask
 
PerlMonks  

Passing anon sub as param

by punkish (Priest)
on Jul 28, 2005 at 20:06 UTC ( [id://479117]=perlquestion: print w/replies, xml ) Need Help??

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

I was recently tackling creating a hierarchical tree of nodes, as in a nested, threaded, discussion list. My data structure would be like (ultimately in a db table) --
my $in = [ {c_id => 1, r_to => 0, dept => 0, p_id => 1, comm => 'this',}, {c_id => 3, r_to => 1, dept => 1, p_id => 1, comm => 'the other',}, {c_id => 5, r_to => 1, dept => 1, p_id => 1, comm => 'else',}, {c_id => 6, r_to => 5, dept => 2, p_id => 1, comm => 'that',}, {c_id => 7, r_to => 6, dept => 3, p_id => 1, comm => 'moreover',}, {c_id => 9, r_to => 3, dept => 2, p_id => 1, comm => 'no way',}, {c_id => 10, r_to => 9, dept => 3, p_id => 1, comm => 'give',}, ];

I would create a sub foo, that would take as input a r_to, traverse the AoH, print out the line, then call itself with the c_id of that line. The result would be

1: this - 3: the other - 9: no way - 10: give - 5: else - 6: that - 7: moreover

I wrote a bunch of code, messed it up, and then went about looking for examples. I found the oh so wise BrowserUk's contribution in the node Re: Cleaner code to build hierarchical display of page names funnily called traverse! A little bit of editing, and I had the following working beautifully --

traverse($in,0,0, sub {print ' ' x $_->{dept},'-',$_->{comm},"\n";}); sub traverse { my( $ref, $reply_to, $depth, $code) = @_; for my $node ( grep{ ($_->{dept} == $depth) && ($_->{r_to} == $reply_to) } @$ref ) { local $_ = $node; $code->(); traverse($ref, $node->{c_id}, ($depth + 1), $code); } }

Then I started wondering why the anon sub was being passed as a param to the sub traverse. Not knowing why, I decided to remove it like so.

traverse($in, 0, 0); sub traverse { my( $ref, $reply_to, $depth) = @_; for ( grep{ ($_->{dept} == $depth) && ($_->{r_to} == $reply_to) } @$ref ) { print ' ' x $_->{dept}, '-', $_->{comm}, "\n"; traverse($ref, $_->{c_id}, ($depth + 1)); } }

And that too worked beautifully. So, I ask the following --

  1. Why did BrowserUk include the print code as an anon sub passed as a param to the traverse sub?
  2. Even though I can get rid of it the way I did above, is there any advantage in the way BrowserUk did it? Or, are there situations in which only BrowserUk's method will work while mine will croak.
  3. What is the purpose of local $_ = $node since removing $node and just using $_ worked as well?

In anticipation...

--

when small people start casting long shadows, it is time to go to bed

Replies are listed 'Best First'.
Re: Passing anon sub as param
by ikegami (Patriarch) on Jul 28, 2005 at 20:29 UTC

    (1) To make traverse reusable. You can change the action taken for every node without editing traverse. You can make traverse do two different things without writing two different versions of it. As BrowserUK's mentioned in his post, "By passing a sub to the traverse routine you can make it do things other than just printing."

    (2) Same answer as above. Both will work,

    (3) The sub needs to be provided the node on which it is to perform an action. Normally, the node would have been passed as a parameter. This alternate way is a bit more concise and has less overhead.

      (1) To make traverse reusable. You can change the action taken for every node without editing traverse. You can make traverse do two different things without writing two different versions of it.

      Hence, I could have done the following just as well --

      sub traverse { # do yer stuff _print($_); traverse(..); } sub _print { my ($node) = @_; print ' ' x $node->{dept}, $node->{c_id}, ': ', $_->{comm}, "\n"; }

      Which, I did, and it also works. Hence, passing an anon sub as a param seems to be just a fancy way of doing what I already know MTOWODI, no?

      --

      when small people start casting long shadows, it is time to go to bed

        I personally think that the anon-sub call back is just cleaner looking, but it has a number of other benefits. The biggest of those is that it doesn't break or force you to do ugly things if traverse is in another package.

        Other benefits: you don't clutter up your symbol table with global named functions that are only intended to be used in one place. Also: you can use traverse in different ways in the same application, rather than defining the callback globally.

        I also think that defining a fallback is cleaner in this case: you just check the $code arg for definedness, rather than probing the caller's package. Scope is nice and closed.

        How about I explain by example:
        traverse($in, 0, 0, sub {print ' ' x $_->{dept},'-',$_->{comm},"\n";}) +; ... later in the code... traverse($in, 0, 0, sub {$sth->execute($dept, $comm)});
        By the way, the sub doesn't have to be anon:
        sub _print { print ' ' x $_->{dept},'-',$_->{comm},"\n"; } traverse($in, 0, 0, \&_print);

        It's a feature, it has no side effects, so there's no reason to get rid of it, and there's every reason to keep it. If you don't want to explicitely specify it, create a wrapper:

        sub print_tree { push(@_, 0, 0, sub {print ' ' x $_->{dept},'-',$_->{comm},"\n";}); &traverse; } print_tree($in);

        Passing an action in allows the parent node to determine the action rather than the local node determining the action.


        Perl is Huffman encoded by design.
Re: Passing anon sub as param
by tmoertel (Chaplain) on Jul 28, 2005 at 22:21 UTC
    The reason BrowserUk passed the node-printing code as an anonymous subroutine was to separate the traversal logic from the node-processing logic. With this separation, we can use the same traversal logic for other purposes, and we can use the same node-processing logic with other traversals.

    Consider the following nested-array-style tree and logic to traverse it:

    my $tree = [ 1 , [ 2 , 3 , [ 4 , [ 5 ] , 6 ] ] , 7 ] ; sub traverse { my ($nodefn, $tree, $depth) = @_; $depth ||= 0; $depth++; for (@$tree) { ref() ? traverse( $nodefn, $_, $depth ) : $nodefn->( $_, $depth ); } }
    We can combine this traversal logic with task-specific node-processing logic. For example, to count the nodes in the tree, we might use logic like this:
    sub count_nodes { my $count; traverse( sub { $count++ }, @_ ); $count; } print count_nodes($tree), $/; # 7
    For other jobs we can easily "plug in" other node-processing code. Here is code to print a single indented node:
    sub print_node { my ($leaf, $depth) = @_; print " " x ($depth - 1), "* $leaf", $/; }
    We just plug it into traverse to print the tree's outline:
    traverse( \&print_node, $tree ); # * 1 # * 2 # * 3 # * 4 # * 5 # * 6 # * 7
    Going further, we can plug in new traversal logic, too. Here is logic to do a breadth-first traversal – all the leaves at each depth are processed before going deeper.
    sub bfs_traverse { my ($nodefn, $tree, $depth) = @_; $depth ||= 0; $depth++; my @leaves = grep !ref(), @$tree; my @branches = grep ref(), @$tree; $nodefn->( $_, $depth ) for @leaves; bfs_traverse( $nodefn, [map @$_, @branches], $depth ) if @branches; }
    Now we can use this traversal logic with our existing print_node logic to outline the tree in breadth-first order:
    bfs_traverse( \&print_node, $tree ); # * 1 # * 7 # * 2 # * 3 # * 4 # * 6 # * 5
    By this point, I hope you can see the answer to your second question: by keeping traversal and node-processing concerns separated and by combining them through a well-defined interface (here, function passing), we can mix and match logic. If we want a breadth-first outline, we don't need to write a breadth-first outline function; we can just combine a breadth-first traversal with a node-printing processor.

    Cheers,
    Tom

Re: Passing anon sub as param
by polettix (Vicar) on Jul 28, 2005 at 23:27 UTC
    You can take the "embedded" approach if it solves your problem. No need to over-complexify your design by making things orthogonal, assured. But you have to make a promise to yourself: that the very one day that you're going to traverse the data structure to do other things - what about searching an item? - you will not cut-and-paste the code, changing only the line with the print to do something else.

    This is called refactoring. It's when you understand that you already solved that problem in one point of your code, and you do a favour to yourself and don't cut-and-paste, but you rather "summarize" what the two functions have in common into a common function that you call twice, in two different context and for two similar-but-different purposes. Why is this a favour to yourself? Because the very time you'll find a bug, you will not have to go hunting in your code to look for all places where you pasted the bug. Because you learn to give these factored-out functions meaningful names, that make your code easier to read and maintain. Because you'll be able to use the same function the next time you have a similar problem.

    Sometimes this factoring excercise is done ahead, like in BrowserUK case. This normally happens when you "see" that an algorithm you're going to use would be really helpful in much other situations, and you prepare to reuse. But be lazy, don't look too much ahead, or you could possibly drawn in a sea of pain, where you spend most of the time in building up a beautiful library, and take 5x the needed time to solve your problem.

    To be fair, this is what I've summarised from some answers to a previous question of mines: Writing general code: real world example - and doubts! (just to warn you, the first answers actually deal with the issue, then you can find interesting suggestions about tainting, but I understand that these would be a little OT here :)

    Flavio
    perl -ple'$_=reverse' <<<ti.xittelop@oivalf

    Don't fool yourself.

Log In?
Username:
Password:

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

How do I use this?Last hourOther CB clients
Other Users?
Others cooling their heels in the Monastery: (6)
As of 2024-03-28 15:52 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found