Beefy Boxes and Bandwidth Generously Provided by pair Networks
Think about Loose Coupling
 
PerlMonks  

Turning foreach into map?

by ghenry (Vicar)
on Apr 04, 2005 at 20:12 UTC ( #444783=perlquestion: print w/replies, xml ) Need Help??

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

Dear all,

I have a simple subroutine. I would like to try and swap out the foreach loop and use map instead:

sub links { my ($url, @list) = @_; my @return; foreach (@list) { push @return, "$url/$_\n"; } return @return; }

Is this right way to do it?

sub links { my ($url, @list) = @_; my @return = map ("$url/$_\n") @list; return @return; }

I have never used map before and I have read the map function page on http://perldoc.perl.org/functions/map.html, but I don't seem to quite understand.

Thanks.

Walking the road to enlightenment... I found a penguin and a camel on the way.....
Fancy a yourname@perl.me.uk? Just ask!!!

Replies are listed 'Best First'.
Re: Turning foreach into map?
by tlm (Prior) on Apr 05, 2005 at 00:19 UTC

    I have never used map before...

    Once you start, you won't stop! You'll wonder how you could program without it.

    ...and I have read the map function page on http://perldoc.perl.org/functions/map.html, but I don't seem to quite understand.

    As a first approximation, you can imagine map as being defined something like this:

    sub my_map { my $sub = shift; my @out; for my $element ( @_ ) { push @out, $sub->( $element ); } return @out; }
    This function my_map takes a code ref as its first argument, and returns the array consisting of applying that code ref to each one of the remaining arguments. Very useful! When programming we often want all the elements of some array, but transformed or operated on in some way. my_map (just like map) abstracts the essentials of this procedure, so that the only variables left are the actual input array and the operation that you want performed on each element.

    You would use my_map like this:

    sub square { $_[0] * $_[0] } my @squares = my_map( \&square, 1..3 ); # @squares is now ( 1, 4, 9 )
    or if you don't want to bother defining a sub just to feed it to my_map, you can give it a code ref written just for the ocassion:
    my @squares = my_map( sub { $_[0]*$_[0] }, 1..3 );
    Same thing. In fact, if you have arranged your definitions so that the compiler processes it before it processes the first call to it, you can toss the parens and write
    my @squares = my_map sub { $_[0]*$_[0] }, 1..3;
    which is beginning to resemble the way one uses Perl's map. Since this is such a handy thing to do, for its map Perl provides some "syntactic sugar" to simplify the writing of such expressions, so that for example you don't need to bother writing "sub" before the code ref. Also, in Perl's map, the code ref doesn't get its argument passed through its @_ array like is the case with my_map's $sub; instead map's the code ref picks up its argument from $_. In other words, if we wanted my_map to mimic Perl's map more closely, we could define it like this:
    sub my_map { my $sub = shift; my @out; for ( @_ ) { push @out, $sub->(); # $sub will get its arg from $_ } return @out; }
    The only difference between this and the previous definition of my_map is in the first and second lines of the loop: 1) the loop variable is no longer $element, but $_; and 2) $sub is now called without arguments, since it will read its arguments from $_. Now we can write
    my @squares = my_map sub { $_ * $_ }, 1..3;
    and that's starting to look a lot like the standard
    my @squares = map { $_ * $_ } 1..3; # BLOCK form of map

    Now, watch this. If you defined a function that operated on $_, e.g.

    sub twice { $_ * 2 }
    you could just feed it to Perl's map as its first argument:
    my @evens = map &twice, 1..10; # EXPR form of map
    and you could also feed it to (the second version of) my_map
    my @evens = my_map \&twice, 1..10;
    Notice that now the only difference that remains between the syntax of the two calls is that with my_map you need to explicitly take the reference to the sub you are passing as the first argument, hence the extra backslash. Can we get rid of this difference too? Yes, by using prototypes. I for one never use prototypes, so I'm not up on all their ins and outs, but for the sake of this illustration
    sub my_map (\&@_) { my $sub = shift; my @out; push @out, $sub->() for @_; return @out; }
    (Note, aside from the prototype, there is no significant difference between this definition of my_map and the previous one; I just condensed it a little bit.) Now you can write,
    my @evens = my_map &twice, 1..10;
    OK, this last version of my_map does a limited impersonation of the "expression form" of map. A slight variant could be used to implement an impersonation of the "block form" of map; the only difference is in the prototype, but just to be clear, here it is in full:
    sub my_map (&@) { my $sub = shift; my @out; push @out, $sub->() for @_; return @out; }
    With this final version of my_map you can write:
    my @evens = my_map { $_ * 2 } 1..3;
    Note that even though we are using an anonymous code ref, the sub keyword is gone. This is syntactically identical to the "block form" of map.

    Alas, my_map is not as versatile as map. It can't do some things that map can do, like this:

    my @evens = my_map $_ * 2, 1..10; # splat!
    and it can't mimic both the "expression form" and the "block form" of map. But I think that by studying my_map you'll get a pretty good feel for what map is doing. That's the important thing; the other stuff with tweaking the definition of my_map so it's usage better resembles that of map is a bit of a distraction. The important thing is to understand that map takes a function as its first argument, and returns the array obtained from applying that function to the rest of its arguments.

    OK, since I've made this far, I might as well tell you, if not the whole story, at least all that I know about map, and the only thing that's left is that both map and my_map have the potential to change their inputs. If, for example, you did this (using the last version of my_map):

    my @ints = 1..3; my @evens = my_map { $_ *= 2 } @ints; print "@evens\n"; print "@ints\n"; __END__ 2 3 6 2 3 6
    ...the input array @ints has been modified by my_map. Same thing with Perl's map, so watch out.

    Last thing: you could play the same game with grep:

    sub my_grep (&@) { my $sub = shift; my @out; $sub->() && push @out, $_ for @_; return @out; }

    the lowliest monk

      Wow! This will take me a while to properly digest, but my first question is, why is:

      my ($url, @list) = @_;

      now:

      my $sub = shift;

      I have seen loads of people do this. Even merlyn now suggests it in another thread that I can't remember, when it's his book I learnt it from ;-)

      Walking the road to enlightenment... I found a penguin and a camel on the way.....
      Fancy a yourname@perl.me.uk? Just ask!!!
        Arguments are passed "by reference" in perl subs, allowing in-place modification. So, you can do things like the following:
        sub double_it { $_[0] *= 2 } my $x = 1; print "\$x is $x\n"; double_it($x); print "\$x is now $x\n";
        and have
        $x is 1 $x is now 2
        When you use:
        my ($url, @list) = @_;
        you're creating copies of the input arguments, so when you modify them you don't have any side-effect outside the function. In your code, these copies are inefficient and not needed, because the code is quite simple and you don't perform in-place modifications. So, you simply grab the first parameter, and use the resulting @_ instead of @list, saving space and time:
        sub links { my $url = shift; # This removes the first element of @_ map { "$url/$_" } @_; # You don't need return :) }
        I also find this construct useful when porting functions into classes. When you transform a plain function in a class/object method, the first parameter that's passed is the class name/reference to object, so if you have a function:
        sub some_func { # ... some stuff? my ($foo, $bar, $baz) = @_; # ... other stuff }
        you can transform it at once:
        sub some_func { my $self = shift; # Maybe do some checks with $self first? # ... some stuff? my ($foo, $bar, $baz) = @_; # ... other stuff }
        in a much cut-and-paste fashion. But do check the code!

        Flavio (perl -e "print(scalar(reverse('ti.xittelop@oivalf')))")

        Don't fool yourself.

        They're both correct; it all depends on what you want to do. In the example I gave, I was re-implementing Perl's built-in map, not your links function. What my code does is to pluck out the first argument from the arguments list, and later process the remaining arguments directly from the arguments list.

        the lowliest monk

      OK, I've read all this now, and I have to be honest, I haven't got as far as references yet, only as far as reading perlreftut, but not actually coded anything.

      I'll get back to you ;-)

      Walking the road to enlightenment... I found a penguin and a camel on the way.....
      Fancy a yourname@perl.me.uk? Just ask!!!

        OK, I've read all this now, and I have to be honest, I haven't got as far as references yet, only as far as reading perlreftut, but not actually coded anything.

        References are far more important than map; if I were you I'd put map aside for a bit and focus on becoming very comfortable with refs.

        the lowliest monk

Re: Turning foreach into map?
by brian_d_foy (Abbot) on Apr 04, 2005 at 21:14 UTC

    Once you decide to use map, you probably don't need the subroutine anymore: you're not hiding that much code. Previously, you had a subroutine call like this:

    my @munged = links( $url, @links );

    To replace links() with an inline map() is only a few characters longer, and it tells the programmer exactly what is happening.

    my @munged = map { "$url/$_" } @links;

    However, if link() does a lot of other stuff you aren't showing us, then you may still want to use it to hide things.

    As a bonus, the URI module can turn relative URLs into absolute ones for you. :)

    --
    brian d foy <brian@stonehenge.com>

      As usual, I have learned a lot from your replies. Thanks.

      dragonchild, I never got a chance to test it, only post this question, as I had my son on my lap ;-) He's now in bed, so I can reply.

      The reason I posted this, is because I am going back to my old programs after learning more, and trying to make them clean and have less code.

      The program in question was written for my wife, as she always asks me how to resize images and upload them to ebay. I wrote this, called ebaypics, which is then saved in ~/.gnome2/nautilus-scripts she then right clicks in the folder she has the photos in, and that's it.

      #!/usr/bin/perl use strict; use warnings; use Net::FTP; use Mail::Sendmail; use Image::Magick; use Term::ProgressBar; ################################################# # program: ebaypics # # license: GPL # # author: Gavin Henry # # # # version: v0.1 # # # # first draft : 22-09-04 # # last update : 27-09-04 # ################################################# my @images = <*.jpg>; my $resize = '640x480'; my ($image,$process); my $ftpsite = 'ftp.somewhere.net'; my $website = 'http://www.somewhere.com/ebay'; my $remotedir = '/htdocs/ebay'; my $username = 'user'; my $password = 'pass'; my $to = '"My name" <my.name@me.co.uk>'; my $from = '"ebaypics" <ebaypics@me.com>'; my $sep = '-' x 76 . "\n"; my $time = localtime; my $progress = Term::ProgressBar->new({name => 'Scaling images', count => scalar @images, ETA => 'linear'}); # Messages print "\n", $sep, "Starting....\n", $sep; &resize; my @thumbs = <thumbnail-*.jpg>; # Get pictures and upload my $ftp = Net::FTP->new("$ftpsite", Timeout => 30) or die "Sorry I can't connect: $@\n"; $ftp->login($username, $password) or die "Wrong password. Please call Gavin.\n"; $ftp->cwd("$remotedir"); foreach (@thumbs) { $ftp->put($_) or die "Can't find the resized pictures: $!\n"; } $ftp->quit() or warn "Couldn't quit. Damn.\n"; my @email_links = links($website, @thumbs); my %mails = ( To => "$to", From => "$from", Subject => "Here are the links to your pictures", Message => "Click on any of the links below to check the pictures" + . " are correct, then copy and paste them into your" . " ebay listing.\n\n" . " Weblinks:\n" . " @email_links\n\n" ); sendmail(%mails); print "\n", $sep, "Resizing of images complete at $time. E-mail sent w +ith website links.\n", $sep; # Subroutines sub resize { print "\n", $sep, "Resizing images.....this may take some time.... +.\n", $sep; foreach (@images) { $image = Image::Magick->new(); $process = $image->Read("$_"); $process = $image->Resize(geometry => "$resize"); warn "$process" if "$process"; $process = $image->Write("thumbnail-$_"); warn "$process" if "$process"; $progress->update(); } print "\n", $sep, "Resizing complete, starting upload to webspace. +....\n", $sep; } # Generate weblinks sub links { my ($url, @list) = @_; my @return; foreach (@list) { push @return, "$url/$_\n"; } return @return; }

      Needs a bit of work ;-)

      Walking the road to enlightenment... I found a penguin and a camel on the way.....
      Fancy a yourname@perl.me.uk? Just ask!!!
Re: Turning foreach into map?
by Roy Johnson (Monsignor) on Apr 04, 2005 at 20:40 UTC
    There are two syntaxes for calling map. If you are passing a block of code, there is no comma between the block and the list. If you are passing an expression, there is a comma. In this case, since it's just a string, I'd recommend the expression form.

    Watch out if your expression is surrounded by parentheses, though, because map will think they enclose all of its arguments. You either need two sets of parens (one around your expression, and one around all of map's args), or you don't need any. In your case, you don't need any.

    return map "$url/$_\n", @list;

    Caution: Contents may have been coded under pressure.

      Watch out if your expression is surrounded by parentheses, though, because map will think they enclose all of its arguments. You either need two sets of parens (one around your expression, and one around all of map's args), or you don't need any.

      You can also get away with the single set of parens if you disambiguate it with +:

      @out = map +( ... ), @in;
      I find myself using this trick all over the place, not only with map and grep but with, e.g. print:
      print +( $foo ? $bar : $baz ) . $quux;
      Without the + above, the last expression would result in the printing of only the value of the expression in the parens. (Although, if warnings are on, perl will say something about interpreting print (...) as a function.)

      the lowliest monk

Re: Turning foreach into map?
by jZed (Prior) on Apr 04, 2005 at 20:23 UTC
    Simply this will do: return map {"$url/$_\n"} @list;

    update note the curly braces instead of parens

    Foreach can be faster than map, so depending on how many times that sub gets called, you may not want to make the change.

      Foreach is faster than map if you're modifying in place. If you're using foreach to push into an array, like what's happening here, then map is going to be faster. (which is why there's the recommendation not to use map if you're not doing anything with the list generated -- you might as well have just done foreach).

      Update: benchmarks to show the speed differences between map, map reassigning to the original list, map in void context, for, and foreach. (if 'recent' is before 5.8.6, I don't think the optimizations worked)

      purple:/tmp oneiros$ perl -version | head -2 This is perl, v5.8.6 built for darwin-thread-multi-2level purple:/tmp oneiros$ perl timer.pl 100: Rate map new_map void_map for foreach map 1089/s -- -16% -37% -61% -61% new_map 1295/s 19% -- -25% -54% -54% void_map 1718/s 58% 33% -- -38% -39% for 2793/s 156% 116% 63% -- -1% foreach 2825/s 159% 118% 64% 1% -- 500: Rate map new_map void_map foreach for map 213/s -- -14% -35% -60% -60% new_map 249/s 17% -- -24% -53% -54% void_map 328/s 54% 32% -- -38% -39% foreach 526/s 147% 112% 61% -- -2% for 538/s 153% 116% 64% 2% -- 2500: Rate map new_map void_map for foreach map 38.5/s -- -15% -38% -61% -61% new_map 45.4/s 18% -- -27% -54% -55% void_map 61.9/s 61% 37% -- -38% -38% for 99.5/s 158% 119% 61% -- -0% foreach 100.0/s 160% 120% 61% 0% --
        Foreach is faster than map if you're modifying in place. If you're using foreach to push into an array, like what's happening here, then map is going to be faster. (which is why there's the recommendation not to use map if you're not doing anything with the list generated -- you might as well have just done foreach).
        This is no longer the case for recent versions of perl. Now map has been optimized in void context, so if you don't do anything with the return values, it doesn't bother generating them.

        In theory, the only difference between map and for/foreach now is a matter of taste/style. (I say "in theory", because I haven't benchmarked it, and sometimes the perl porters *think* they've done, isn't quite what they've done.)

      Foreach can be faster than map, so depending on how many times that sub gets called, you may not want to make the change.

      Proof? Caveats? Situations? You can't just throw that statement out there and expect it to stand on its merits ...

        use strict; use warnings; use Benchmark (); sub t_for_r { my ($url, $list) = @_; my @return; push(@return, "$url/$_\n") foreach @$list; return @return; } sub t_map_r { my ($url, $list) = @_; my @return = map { "$url/$_\n" } @$list; return @return; } sub t_map2_r { my ($url, $list) = @_; return map { "$url/$_\n" } @$list; } sub t_for_a { my $url = shift; my @return; push(@return, "$url/$_\n") foreach @_; return @return; } sub t_map_a { my $url = shift; my @return = map { "$url/$_\n" } @_; return @return; } sub t_map2_a { my $url = shift; return map { "$url/$_\n" } @_; } { my $url = 'http://www.domain.com/'; my @list = qw( file0 file1 file2 file3 file4 file5 file6 file7 file8 file9 ); Benchmark::cmpthese(-3, { t_for_r => sub { $a = join('', t_for_r ($url, \@list)); }, t_map_r => sub { $a = join('', t_map_r ($url, \@list)); }, t_map2_r => sub { $a = join('', t_map2_r($url, \@list)); }, t_for_a => sub { $a = join('', t_for_a ($url, @list)); }, t_map_a => sub { $a = join('', t_map_a ($url, @list)); }, t_map2_a => sub { $a = join('', t_map2_a($url, @list)); }, }); } __END__ Rate t_for_r t_map_r t_for_a t_map_a t_map2_r t_map2_a t_for_r 20641/s -- -3% -5% -5% -33% -36% t_map_r 21196/s 3% -- -2% -2% -31% -34% t_for_a 21651/s 5% 2% -- -0% -29% -33% t_map_a 21684/s 5% 2% 0% -- -29% -32% t_map2_r 30598/s 48% 44% 41% 41% -- -5% t_map2_a 32087/s 55% 51% 48% 48% 5% --

        At least for small lists, t_for and t_map are the same, and t_map2 is much faster.

        Passing by ref vs passing the array doesn't matter if you use @_ directly.

Re: Turning foreach into map?
by dragonchild (Archbishop) on Apr 04, 2005 at 20:24 UTC
    Did it work? If it didn't, what did you see that you think you shouldn't have? In other words, it looks good, but YMMV.

    FYI, I would have written that as so:

    sub links { my $url = shift; return map { "$url/$_\n" } @_; }
Re: Turning foreach into map?
by thor (Priest) on Apr 04, 2005 at 23:16 UTC
    I would like to try and swap out the foreach loop and use map instead.
    Do you have a reason other than "map is cooler than foreach" for doing this? Keep in mind, learning is a valid reason to do something...just not in something that someone is paying you for.

    thor

    Feel the white light, the light within
    Be your own disciple, fan the sparks of will
    For all of us waiting, your kingdom will come

      Yes.

      I am learning and wanted to try a different way of doing a foreach. That's all.

      Walking the road to enlightenment... I found a penguin and a camel on the way.....
      Fancy a yourname@perl.me.uk? Just ask!!!
A hint about using map
by doom (Deacon) on Apr 05, 2005 at 23:17 UTC
    One of the things you always want to do with map is to use it to do a transformation on a list of strings.

    At some point you're guaranteed to try something like this, but it won't work:

    # WRONG @new_list_wrong = map{ s/that idiot/our esteemed leader/ } @list;
    This is because s/// returns the *count* of the changes, rather than the changed string (most people agree this is a DWIM violation: this is an area where Larry blew it).

    What actually works is something like this:

    @new_list_right = map{ s/that idiot/our esteemed leader/; $_ } @list;

    There's still another gotcha here though, in that if you look at the original @list after doing this, you'll find that it was transformed in exactly the same way as the new list. Each item in the @list array is aliased to $_, so it can be changed by running map on it. (Of course, you knew this. But you'll still get bitten by it on occasion.)

    Weirdly enough, the m// operator doesn't have the same problem as s/// does: m// does pretty much what you'd expect in list context:

    # yet another way to strip leading and trailing whitespace: @cleaned_list = map{ m/^\s*?(\w.*?)\s*?$/ } @list;

    Trivia: perl is well known for having ripped off features form shell and awk, but map was lifted from lisp.

      most people agree this is a DWIM violation: this is an area where Larry blew it
      It may not be a DWIM for you, but it's a DWLM (do what Larry means), and that's the only meaning that matters. In fact, I don't think it would look right for it to return the changed string. I like that I can use it as:
      if (s/foo/bar/) { # yes, I replaced foo with bar ... }
      So, it certainly DWIMs for me. Apparently, I'm not one of "most" in your sentence. However, a lot of people have learned from my books, so I suspect the number who understand it the the way I do would be a large portion of your minority.

      -- Randal L. Schwartz, Perl hacker
      Be sure to read my standard disclaimer if this is a reply.

        most people agree this is a DWIM violation: this is an area where Larry blew it
        It may not be a DWIM for you, but it's a DWLM (do what Larry means), and that's the only meaning that matters. In fact, I don't think it would look right for it to return the changed string. I like that I can use it as:
        if (s/foo/bar/) { # yes, I replaced foo with bar ... }
        So, it certainly DWIMs for me.
        Right, that's a point, but the way I look at it it would still work even if s/// was returning the changed string (except, of course, when the returned string was something like zero or an empty string).

        So the actual behavior does a slightly better job of DWIM in one case, and a worse job in another case... (and now that I think of it, why not return the string in list context and the count in scalar. Wouldn't that cover both bases?).

        Update: actually, there's another problem I haven't thought about -- if you're doing a map{ s///; $_ } you've got the string passed through map even if there's no match. If you had s/// return the string *only* if it were changed, then you could do an if(s///) with it (usually) but you'd probably end up a sticking a $_ on the end of your map blocks anyway, just to get the string passed through in the no match condition.

        s/where Larry blew it/where Larry's brilliance is more difficult to perceive/

      One of the things you always want to do with map is to use it to do a transformation on a list of strings.
      Huh?
      @findings = map { $_->{$x} ? [ $_->{$y}, keys %$_ ] : () } @hashrefs;
      Here I'm slinging just references and numbers. And using the map to do a greppy thing at the same time.

Log In?
Username:
Password:

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

How do I use this? | Other CB clients
Other Users?
Others scrutinizing the Monastery: (3)
As of 2023-02-08 23:21 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    I prefer not to run the latest version of Perl because:







    Results (44 votes). Check out past polls.

    Notices?