tags:

views:

88

answers:

3

I'm using Getopt::Lucid to process CLO and I've run into an interesting and unexpected problem. The following code:

push @clo_spec, map { Switch($_) } qw(-c -m -s -p),
                map { Switch($_) } qw(--help --man --usage --version),
                map { Switch($_) } qw(--debug --verbose),
                map { Param($_)  } keys %$rc_spec_ref
;

my $clo_o = Getopt::Lucid->getopt(\@clo_spec);

generates the following error:

'Getopt::Lucid::Spec=HASH(0x9383847)' is not a valid option name/alias

Now, Getopt::Lucid is configured by quoting an string expression representing valid options and then passing those strings to one of six subroutines which return blessed hashes. Each subroutine represents a type of option; switch, counter, parameter, list or key-pair.

The interesting part is that if any three map expressions are removed,

push @clo_spec, #map { Switch($_) } qw(-c -m -s -p),
                map { Switch($_) } qw(--help --man --usage --version),
                #map { Switch($_) } qw(--debug --verbose),
                #map { Param($_)  } keys %$rc_spec_ref
;

then everything works fine. The even more interesting part is that if you encase each map expression in parentheses, everything also works fine:

push @clo_spec, (map { Switch($_) } qw(-c -m -s -p)),
                (map { Switch($_) } qw(--help --man --usage --version)),
                (map { Switch($_) } qw(--debug --verbose)),
                (map { Param($_)  } keys %$rc_spec_ref)
;

The above leads me to believe that this problem isn't related to a bug in Getopt::Lucid. Also, I considered the above fix after looking at the map function's reference which mentions that sometimes map can get confused with commas. Perl will flatten embedded lists, and the surrounding parentheses seem to have the effect of delineating each map expression, but I really don't understand what's going on.

Can someone please explain?

+5  A: 

What is I think is happening is that without the parens, the output one each map is fed as arguments to the map preceding it. Say you only had two of these maps:

push @clo_spec, (map { Switch($_) } qw(-c -m -s -p)),
                (map { Switch($_) } qw(--help --man --usage --version)),

The last one executes first, feeding Switch '--help', '--man', etc. Switch returns the Hashes you expect. Then, the first map executes, feeding it's switch the '-c', '-m', 's' and '-p'. But then it also feeds it the result of the first switch, which explains why you're getting errors that HASH(...) isn't a valid option name.

The solution? Either use parens to make the arguments to each map explicit[1], or use multiple push lines, one for each map.

[1] If you do use parens, I'd recommend instead of (map ....) writing map(....), as it would be clearer why the parens are there.

zigdon
I like to make as few calls as possible to functions even if in practice there are trivial speed and memory issues. Once you guys explained what was going on, map(...) isn't just better style but I think it makes more sense too.
gvkv
+10  A: 

The map function takes a list as an argument and generates a list as a result. You can chain map statements together (feeding the output of one map as the input to another) which is what your first example does. Adding parentheses around the individual map operators breaks the chain.

When reading chained map (or grep) statements, read from right to left.

push @clo_spec,
    map { Switch($_) } qw(-c -m -s -p),
    map { Switch($_) } qw(--help --man --usage --version),
    map { Switch($_) } qw(--debug --verbose),
    map { Param($_)  } keys %$rc_spec_ref;

The last map calls Param() for each key from %$rc_spec_ref and returns the results. The map above that calls Switch() for the values --debug, --verbose, and each result from the last map. The map blocks above those get even longer argument lists with the flags in qw() having the results of the other map blocks concatenated with them.

Adding parentheses around each map block changes the way the code parses, causing each map to be treated individually rather than being daisy-chained.

Michael Carman
Right associativity with map and grep (and other functions?)! I didn't even consider that but I suppose it makes sense since it doesn't know what it's going to find after the block and thus has to expand what's rightmost first.
gvkv
Associativity applies to binary operators (in infix notation). `map` and `grep` are functions. Perl allows you to omit the parentheses for most built-in functions. When you do that the function's prototype determines how the expression parses. The prototype for `map` says that the first argument is a block to execute and everything else is data to consume. The number of data elements isn't fixed, so Perl consumes everything unless you use the `map(...)` syntax to explicitly delimit the argument list.
Michael Carman
+6  A: 
$ perl -MO=Deparse
push @clo_spec, map { Switch($_) } qw(-c -m -s -p),
                map { Switch($_) } qw(--help --man --usage --version),
                map { Switch($_) } qw(--debug --verbose),
                map { Param($_)  } keys %$rc_spec_ref
;

my $clo_o = Getopt::Lucid->getopt(\@clo_spec);
^D
push @clo_spec, map({Switch($_);} ('-c', '-m', '-s', '-p'), map({Switch($_);} (
'--help', '--man', '--usage', '--version'), map({Switch($_);} ('--debug',
'--verbose'), map({Param($_);} keys %$rc_spec_ref))));
my $clo_o = 'Getopt::Lucid'->getopt(\@clo_spec);
- syntax OK

If you're ever confused by how Perl is parsing something, B::Deparse is terrific.

In this case, you can clearly see that each map is operating not just on the qw() you've given it, but also the results of the map following it.

ephemient
Cool. Thanks for the heads-up on B::Deparse.
gvkv