http://qs321.pair.com?node_id=958673

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

I am using 5.14.2
use strict; my %x = map { $_ => 1 } qw(a b c);
works, however
use strict; my %y = map { "prefix_$_" => 1 } qw(a b c);
does not compile ("not enough arguments for map").

Is this a bug or am I being stupid?

Replies are listed 'Best First'.
Re: map problem
by moritz (Cardinal) on Mar 09, 2012 at 10:57 UTC

    This is another instance of the problem that a pair of curlies can stand for an anonymous subroutine or a hash reference. You can disambiguate with a semicolon in the block:

    my %y = map {; "prefix_$_" => 1 } qw(a b c);

    It's more of a design limitation than a bug in perl or in you :-)

      Ok, but why is it neccessary only in the second case?

        The decision to parse an opening curly bracket as a block or a hash ref is really just a heuristic. When the parser sees a string quote, it decides in favor of the hash ref, whereas a variable makes it assume a block.

        Or put differently, the parser cheats as hell, and sometimes that backfires. But not always.

Re: map problem
by GrandFather (Saint) on Mar 09, 2012 at 11:13 UTC

    Not a bug (really) and not stupid, just a subtle foible of Perl that bites occasionally. From the perlfunc documentation for map:

    { starts both hash references and blocks, so map { ... could be either the start of map BLOCK LIST or map EXPR, LIST. Because perl doesn't look ahead for the closing } it has to take a guess at which its dealing with based what it finds just after the {. Usually it gets it right, but if it doesn't it won't realize something is wrong until it gets to the } and encounters the missing (or unexpected) comma. The syntax error will be reported close to the } but you'll need to change something near the { such as using a unary + to give perl some help:
    1. %hash = map { "\L$_", 1 } @array # perl guesses EXPR. wrong
    2. %hash = map { +"\L$_", 1 } @array # perl guesses BLOCK. right
    3. %hash = map { ("\L$_", 1) } @array # this also works
    4. %hash = map { lc($_), 1 } @array # as does this.
    5. %hash = map +( lc($_), 1 ), @array # this is EXPR and works!
    True laziness is hard work
      I also found that adding a string cat operation seemed to clarify things enough for the parser also. I'm not sure which of the various solutions is "the best" if there even is such a thing!
      #!/usr/bin/perl -w use strict; use Data::Dumper; my %x = map {; "prefix_$_" => 1 } qw(a b c); my %y = map { "prefix_"."$_" => 1 } qw(a b c); my %z = map { ''."prefix_$_" => 1 } qw(a b c); print Dumper \%y, \%x, \%z;

        I like a modification of your middle suggestion best:

        my %y = map { "prefix_".$_ => 1 } qw(a b c);
        There's no need to put $_ inside quotes, only to have Perl do variable interpolation on that string. :)

        Alex / talexb / Toronto

        "Groklaw is the open-source mentality applied to legal research" ~ Linus Torvalds

        I like the variant with the semicolon best, because it's the most explicit way to say that something should be parsed as a block. Which conveys a clear message to the maintenance programmer: I wanted a block here, and you'd better leave the semicolon there.

        Relying on the exact semantics of the disambiguation heuristics doesn't leave any traces in the code that there was a problem, and the unsuspecting maintenance programmer will run into the same problems as you did.

Re: map problem
by JavaFan (Canon) on Mar 09, 2012 at 10:58 UTC
    That's because when Perl sees the { following the map, it has to guess whether it's seeing the beginning of a block, or the beginning of a hashref, and it may only keep one token ahead. Which is a string, so map guesses hashref, so, it assumes it's going to parse map EXPR, LIST. And then there's no comma.

    Use:

    my %y = map {;"prefix_$_" => 1 } qw(a b c);
    The semi-colon tells Perl it's a block.
      In my particular case I would prefer this syntax:
      my %y = map { ("x$_" => 1) } qw(a b c);
      or would you say that using the semicolon is already an accepted idiom for such cases?
        I'm not the only one using semi-colons. I think I picked this up from Larry Rossler (the one from the GRT) back in the 1990s.

        But your use of parenthesis still makes it ambiguous. Perl may now guess right, but it's still a guess. {("x$_" => 1)} can still be a hashref. Perl will guess incorrectly if it's written as:

        my @y = map { ("x$_" => 1) }, qw(a b c);
        But feel free to do whatever works for you. Don't do something just because others do it, or not do something because others don't.

        Or you could do it like this:

        my %y = map( ( "x$_" => 1 ), qw( a b c ) );

        The code with the semi-colon

        • has an unambiguous effect (not a valid hash constructor),
        • has an unambiguous purpose (coder obviously doing something to appease the compiler), and
        • is idiomatic.

        { ("x$_" => 1) } is still a valid hash constructor, and it's not clear that the parens are required, so it's not as clear as it could be.

Re: map problem
by ForgotPasswordAgain (Priest) on Mar 09, 2012 at 22:51 UTC

    Everyone already answered the problem. But another thing is I avoid using curly braces except when mapping to hashrefs. In this case:

    my %y = map +( "prefix_$_" => 1 ), qw/a b c/;

    As far as I know, using a block unnecessarily creates a lexical scope, which seems like a waste.

    And speaking of waste, I notice you're pointing your hash to values of 1, probably intending to look at keys(%y) later. That's an unfortunate idiom if you ask me, as it points to a different copy of "1" every time. It's often better (though not important here for 3 values) to alias to undef if you're only concerned about getting a unique list of keys.