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

What I want to do is create a string from an array, joining each element with a different string depending on the values of the elements being joined.

For example, I want to join this list

0,1,0,1

to get this result:

__RESULT__ 0:10:1

My first thought was to assume that join worked iteratively on the elements of the array, and so if I had a matching array I could do this

my @a = (':','',':',''); my $i=0; print join $a[$i++], 0,1,0,1; # Wrong, $i isn't incremented

But $i is not incremented during the execution of join, and so the first element of @a is used to concatenate all my list items:

__RESULT__ 0:1:0:1

If join did work iteratively, I would expect it to set $_ for each iteration, and if this was true I could do this

print join $_ ? '' : ':', 0,1,0,1; # Wrong, $_ isn't set __RESULT__ 0:1:0:1

I also realised that I might be thinking naively about the iteration - that each element is not handled in isolation, but in pairs like this

(start) 1. join $a[0] and $a[1] 2. join $a[1] and $a[2] 3. join $a[2] and $a[3] (finished)

And that if indeed join works on pairs of elements, setting $_ doesn't make sense - which element would be used for each iteration? (As a side thought I wonder if it would make sense to set $a and $b, like in a sort.)

But that doesn't seem to be how join works. I suppose it could also be that join does work iteratively, and on pairs, but that it only evaluates the EXPR part of join just once.

I can just forget about join and rewrite like this

my $j = ''; $j .= $_ ? length $j ? ":$_" : $_ : $_ for (0,1,0,1); print $j; __RESULT__ 0:10:1

But I'm surprised that there isn't some join trick that would do the job more succinctly.

Am I thinking straight? Please tell me what I'm overlooking!

 

Replies are listed 'Best First'.
Re: Surprised by join
by halley (Prior) on Jun 08, 2004 at 13:19 UTC
    In brief, join() is a plain function. It does not evaluate its first argument (or any argument) more than once. In your example, $i is incremented, but after the join function has been called and returns.

    I'm pretty sure that the only things that evaluate an argument more than once will accept a BLOCK type argument. For example, sort BLOCK LIST or map BLOCK LIST. Curly braces. A bit of code to be evaluated on each iteration.

    my $result = ''; my @list = qw(one two three four five six seven eight); $result .= (shift @list).':'.(shift @list) while @list; print $result, $/; __OUTPUT__ one:twothree:fourfive:sixseven:eight

    --
    [ e d @ h a l l e y . c c ]

      I gathered this, and I understand the post increment, what I'm wondering about is sensible alternatives to my final code. I accept that join might not be the right tool, but surely there is something better than my concatenation in a for loop, or your while loop?

      PS: stop changing your node, I don't know what I'm replying to! :)

       

Re: Surprised by join
by eserte (Deacon) on Jun 08, 2004 at 13:44 UTC
    How about this myjoin function:
    sub myjoin (&@) { my $sub = shift; my $res = $_[0]; for my $i (1 .. $#_) { $res .= $sub->($_[$i-1], $_[$i]) . $_[$i]; } $res; } my @a = (':','',':',''); my $i=0; print myjoin { $a[$i++] } 0,1,0,1; # Right now
    You can even change the separator depending of its neighbors, which are supplied as arguments to the code block.

      ++ I like this, although I'm starting to think that I had just misunderstood join and should probably go with an alternate solution rather than shoehorn join to my way of thinking.

       

        Somewhere else in this thread $a and $b were mentioned. The snippet above could be changed to use this. Instead of

        $res .= $sub->($_[$i-1], $_[$i]) . $_[$i];

        it would be

        local $a = $_[$i-1]; local $b = $_[$i]; $res .= $sub->() . $_[$i];
        (Untested)
      Your approach was exactly what the OP was looking for (++), but your example has some excess baggage that made it confusing for me. Simplified:
      sub myjoin (&@) { my $sub = shift; my $res = shift; $res .= $sub->() . $_ for (@_); $res; }
      Update: Oh, I see: the arguments to sub were to stand in for $a and $b (a la sort), in case the user wanted to have that kind of flexibility in the sub.

      Is the thread title a C.S. Lewis reference, I wonder?


      The PerlMonk tr/// Advocate
        Is the thread title a C.S. Lewis reference, I wonder?

        It is, well spotted.

         

Re: Surprised by join
by itub (Priest) on Jun 08, 2004 at 15:01 UTC

    Another way of doing it: the List::Util::reduce function works more or less the way you expected join to work, except that it's more general and not only for joining

    use List::Util 'reduce'; print reduce { $a . ($b ? ":" : "") . $b } (0,1,0,1);

    From perldoc List::Util :

     reduce BLOCK LIST
               Reduces LIST by calling BLOCK multiple times, setting $a and $b
               each time. The first call will be with $a and $b set to the first
               two elements of the list, subsequent calls will be done by setting
               $a to the result of the previous call and $b to the next element in
               the list.
    
               Returns the result of the last call to BLOCK. If LIST is empty then
               "undef" is returned. If LIST only contains one element then that
               element is returned and BLOCK is not executed.
    
                   $foo = reduce { $a < $b ? $a : $b } 1..10       # min
                   $foo = reduce { $a lt $b ? $a : $b } 'aa'..'zz' # minstr
                   $foo = reduce { $a + $b } 1 .. 10               # sum
                   $foo = reduce { $a . $b } @bar                  # concat
    

    List::Util is a core module since perl-5.7.3.

Re: Surprised by join
by Abigail-II (Bishop) on Jun 08, 2004 at 15:20 UTC
    You could tie $":
    #!/usr/bin/perl use strict; use warnings; sub TIESCALAR { my $class = shift; bless [-1 => [@_]] => $class; } sub STORE {die} sub FETCH { ${$_ [0]} [1] [${$_ [0]} [0] ++ % @{${$_ [0]} [1]}]; } tie $" => main => ":", "-"; my @a = qw /0 1 0 1 0 1 0 1 0 1 0 1/; print "@a\n"; __END__ 0:1-0:1-0:1-0:1-0:1-0:1
    Unfortunally, there are a couple of bugs in perl related to this. First is that it will do a bogus call to FETCH before interpolating (FETCH is called once too often). Second is that perl gets mighty confused if FETCH returns an empty string. (Which is unfortunally just what you want).

    Abigail

Re: Surprised by join
by pelagic (Priest) on Jun 08, 2004 at 13:40 UTC
    I don't know what your rules are to have a ':' or a '' but what about a nice map statement?
    $result = join /""/, (map {$_, $_ ? '' : ':'} @list);

    pelagic

      Yes indeed, map also does the trick, I'm just surprised that join doesn't, not without help anyway.

       

        join does not set $_ to each element obviously. And why is that so? Ask Larry!

        pelagic