evil_otto has asked for the wisdom of the Perl Monks concerning the following question:
How can I map a list (a,b) into (a,fn(b)) in the context of a ternary operator?
Here's my full problem: I have a string like "a,b=c,e=#f" - comma separated terms, where each is of the form "x", "x=y", or "x=#y". I need to parse this string into a list (really, a hash), where "x" becomes x=>fn(x), "x=y" becomes x=>y, and x=#y becomes x=>fn(y). The silly part is that I'd like to do it as a ternary operator, so I can assign it without needing to put an empty declaration of the variable outside an if block.
The first two parts, x=>fn(x) and x=>y are easy:
@result=map {
/=/ ? split(/=/,$_,2) :
($_,fn($_)) } split(/,/, $string);
Adding in the x=>fn(y) case is where I'm having trouble; my first attempt:
@result=map {
/=#/? (@l=split(/=#/,$_,2) && ($l[0],fn($l[1])) :
/=/ ? split(/=/,$_,2) :
($_,fn($_)) } split(/,/, $string);
didn't quite work, and in the course of trying to debug the first branch of the ternary I got segfaults from perl:
$ perl -e '
(@n=(1,2) && ($n[1],$n[0]));
(@n=(1,2) && ($n[1],$n[0]));
(@n=(1,2) && ($n[1],$n[0]));
'
Segmentation fault
Is this just perl trying to tell me that this is a stupid way to go about this?
Re: map a list
by almut (Canon) on Mar 12, 2007 at 19:27 UTC
|
If you really want to write it that way, I think you'd need to use
do {...}, e.g. something like this
my $s = "a,b=c,e=#f";
sub fn { "fn(@_)" } # dummy function
my %result = map {
/=#/ ? do { my @l = split(/=#/,$_,2); ( $l[0], fn($l[1]) ) }
: /=/ ? split(/=/,$_,2)
: ($_,fn($_))
}
split(/,/, $s);
use Data::Dumper;
print "$s\n";
print Dumper \%result;
Output:
a,b=c,e=#f
$VAR1 = {
'e' => 'fn(f)',
'a' => 'fn(a)',
'b' => 'c'
};
| [reply] [d/l] [select] |
|
This is exactly what my brain was trying to do the first time around, I just couldn't think of using a "do" block (I was thinking of the c comma operator, which obviously does something quite different in perl list context)
| [reply] |
Re: map a list
by dragonchild (Archbishop) on Mar 12, 2007 at 19:08 UTC
|
my @accumulator;
foreach my $item ( split ',', $string ) {
my @elems = split '=', $item;
if ( @elems == 1 ) {
push @accumulator, "fn($item)";
}
else {
if ( $elems[1].substr(0,1) eq '#' ) {
push @accumulator, $elems[0] . '=>fn(' . $elems[1] . ')';
}
else {
push @accumulator, "$elems[0]=>$elems[1]";
}
}
}
My criteria for good software:
- Does it work?
- Can someone else come in, make a change, and be reasonably certain no bugs were introduced?
| [reply] [d/l] |
Re: map a list - segfault
by imp (Priest) on Mar 12, 2007 at 20:00 UTC
|
The block that caused the segfault doesn't work the way you think it does, because of precedence:
perl -MO=Deparse -e '
(@n=(1,2) && ($n[1],$n[0]));
(@n=(1,2) && ($n[1],$n[0]));
(@n=(1,2) && ($n[1],$n[0]));
'
@n = ('???', 2) && ($n[1], $n[0]);
@n = ('???', 2) && ($n[1], $n[0]);
@n = ('???', 2) && ($n[1], $n[0]);
Using 'and' instead of && would work, or an extra set of parens.
perl -MO=Deparse -e '
(@n=(1,2) && ($n[1],$n[0]));
((@n=(1,2)) && ($n[1],$n[0]));
(@n=(1,2) and ($n[1],$n[0]));
'
@n = ('???', 2) && ($n[1], $n[0]);
$n[1], $n[0] if @n = (1, 2);
$n[1], $n[0] if @n = (1, 2);
As for the segfault itself, I don't grok why it breaks but Devel::Peek gives a hint:
perl -MDevel::Peek -le '
(@n=(1,2) && ($n[1],$n[0])); print Dump \@n;
(@n=(1,2) && ($n[1],$n[0])); print Dump \@n;'
SV = RV(0x3c02b018) at 0x3c0062dc
REFCNT = 1
FLAGS = (TEMP,ROK)
RV = 0x3c011718
SV = PVAV(0x3c0084dc) at 0x3c011718
REFCNT = 2
FLAGS = ()
IV = 0
NV = 0
ARRAY = 0x3c02a4f0
FILL = 1
MAX = 3
ARYLEN = 0x0
FLAGS = (REAL)
Elt No. 0
SV = NULL(0x0) at 0x3c006168
REFCNT = 1
FLAGS = ()
Elt No. 1
SV = NULL(0x0) at 0x3c006264
REFCNT = 1
FLAGS = ()
SV = RV(0x3c02b018) at 0x3c0062dc
REFCNT = 1
FLAGS = (TEMP,ROK)
RV = 0x3c011718
SV = PVAV(0x3c0084dc) at 0x3c011718
REFCNT = 2
FLAGS = ()
IV = 0
NV = 0
ARRAY = 0x3c02a4f0
FILL = 1
MAX = 3
ARYLEN = 0x0
FLAGS = (REAL)
Elt No. 0
SV = UNKNOWN(0xff) (0x0) at 0x3c006168
REFCNT = 1
FLAGS = ()
Elt No. 1
SV = UNKNOWN(0xff) (0x0) at 0x3c006264
REFCNT = 1
FLAGS = ()
Note that the array entries changed from :
SV = NULL(0x0) at 0x3c006168
To:
SV = UNKNOWN(0xff) (0x0) at 0x3c006168
| [reply] [d/l] [select] |
Re: map a list
by GrandFather (Saint) on Mar 12, 2007 at 19:19 UTC
|
use strict;
use warnings;
my $str = "a,b=c,e=#f";
my @result = map {
/(\w+)(=(#?)(\w+))?/;
defined $3 && $3 eq '#' ? "$1=>fn($4)" :
defined $2 ? "$1=>$4"
: "$1=>fn($1)"
} split ',', $str;
print "@result";
Prints:
a=>fn(a) b=>c e=>fn(f)
DWIM is Perl's answer to Gödel
| [reply] [d/l] [select] |
|
| [reply] [d/l] |
|
Argh, I did know better, I just forgot. However that's not a style I'd recommend for practical code in any case - it would be a dog to maintain and is somewhat obscure. Inserting die if ! in front of the regex might suit the OP's purpose.
DWIM is Perl's answer to Gödel
| [reply] [d/l] |
|
Ah! While this answer is not ideal (it doesn't preserve the case : action style of the original, which makes it more straightforward to add in another syntax when that becomes necessary), it did provide the insight for achieving wisdom: since I'm already matching the expression with a RE, I can simply use the work already done and avoid re-splitting. So:
my @result=map {
/(\w+)=#(.+)/ ? ($1,fn($2)) :
/(\w+)=(.+)/ ? ($1, $2) :
($_, fn($_))
} split(/,/, $str);
Thanks.
Still curious about that segfault tho... | [reply] [d/l] |
|
Depending on what the larger problem is, you may be better looking at something like Parse::RecDescent. Even if you stick to manual parsing, using a more explicit coding structure as shown in dragonchild's example would pay off in terms of maintenance.
DWIM is Perl's answer to Gödel
| [reply] |
Re: map a list
by davidrw (Prior) on Mar 12, 2007 at 20:09 UTC
|
I'm a fan of the ternary operator, but i think an approach like this would be clearer:
# LHS = Left-hand side
# RHS = Right-hand side
my $s = 'a,b=c,e=#f';
my %h = map {
$_ .= "=fn($_)" unless /=/; # add a RHS
s/#(\w+)/fn($1)/; # change the RHS '#' notation to the '
+fn' notation
split( /=/, $_, 2 ); # return the LHS=>RHS mapping (delim'd
+ by a '=')
} split /,/, $s; # split on commas to get the strings for the pai
+rs
| [reply] [d/l] |
Re: map a list
by BrowserUk (Patriarch) on Mar 12, 2007 at 22:13 UTC
|
#! >perl -slw
use strict;
use Data::Dumper;
$_ = 'a,b=c,e=#f';
s[(\w+)=#(\w+)][$1=>fn($2)]g;
s[(\w+)=(\w+)][$1=>$2]g;
s[(?<!=>)(\w+),][$1=>fn($1),]g;
my %results = split ',|=>';
print Dumper \%results;
__END__
C:\test>junk8
$VAR1 = {
'e' => 'fn(f)',
'a' => 'fn(a)',
'b' => 'c'
};
Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
"Science is about questioning the status quo. Questioning authority".
In the absence of evidence, opinion is indistinguishable from prejudice.
| [reply] [d/l] |
Re: map a list
by ikegami (Patriarch) on Mar 12, 2007 at 21:23 UTC
|
The conditional operator (?:) usually makes code less readable, especially when nested.
my %results = map {
my ($x, $y) = split(/=/, $_, 2);
if (defined($y) && $y =~ /^#(.*)/) {
$y = fn($1);
}
elsif (!defined($y)) {
$y = fn($x);
}
($x, $y)
} split(/,/, $string);
If you wanted terse:
my %results = map {
/^([^=]*)(?:(=)(#)?(.*))?/s;
($1, ($3 ? fn($4) : ($2 ? $4 : fn($1))))
} split(/,/, $string);
Update: Added terse snippet.
| [reply] [d/l] [select] |
Re: map a list
by johngg (Canon) on Mar 12, 2007 at 21:58 UTC
|
I don't think this is the best way to solve the problem but, instead of ternaries, I thought I'd have a go with regex conditionals. It also uses an on-the-fly subroutine method suggested by almut a couple of months ago here.
use strict;
use warnings;
my $str = q{a,b=c,e=#f,ghi};
my @result =
map
{
my $res;
m
{(?x)
^
(?(?=([a-z])$)
(?{(sub {($res) = @_})->(qq{$1=>fn($1)})})
|
(?(?=([a-z])=([a-z])$)
(?{(sub {($res) = @_})->(qq{$2=>$3})})
|
(?(?=([a-z])=\#([a-z])$)
(?{(sub {($res) = @_})->(qq{$4=>fn($5)})})
|
(?{(sub {($res) = @_})->(q{???})})
)
)
)
};
$res
}
split m{,}, $str;
print qq{@result\n};
It prints
a=>fn(a) b=>c e=>fn(f) ???
I hope this is of interest. Cheers, JohnGG | [reply] [d/l] [select] |
|
You can simplify your code a lot by making $res a package variable.
my @result =
map
{
local our $res;
m
{(?x)
^
(?(?=([a-z])$)
(?{ $res = qq{$1=>fn($1)}; })
|
(?(?=([a-z])=([a-z])$)
(?{ $res = qq{$2=>$3}; })
|
(?(?=([a-z])=\#([a-z])$)
(?{ $res = qq{$4=>fn($5)}; })
|
(?{ $res = q{???}; })
)
)
)
};
$res
}
split m{,}, $str;
Of course, the above is just an obfuscated way of doing
my @result =
map
{
(/([a-z])$/
? qq{$1=>fn($2)}
: (/([a-z])=([a-z])$/
? qq{$1=>$2}
: (/([a-z])=\#([a-z])$/
? qq{$1=>fn($2)}
: q{???}
)
)
)
}
split m{,}, $str;
By no means do I consider the second snippet acceptable either.
By the way, why did you introduce limits on what the values can be? Nowhere did the OP imply the values would be single lowercase letters. If you remove that arbitrary limit, you can get rid of the "???" case.
Why did you also changed the output, returning a string where a pair of values should have been returned?
| [reply] [d/l] [select] |
Re: map a list
by Anno (Deacon) on Mar 12, 2007 at 20:14 UTC
|
@result = map {
my ( $x, $y) = split /=#?/;
/=#/ ? ( $x, fn( $y)) :
/=/ ? ( $x, $y) :
( $x, fn( $x));
} split /,/, $string;
See how the code makes the map block a comfortable place, with named lexicals for the parts we work with. The nested ?: can then be written strightforward.
I think an approach more along the lines of dragonchild's (though he solved different problem) would be more readable and maintainable.
Anno
Update: Corrected misattribution to imp | [reply] [d/l] |
|
Perhaps it was a bit unclear, in that I didn't specify that "fn()" is a perl sub defined elsewhere. Otherwise there is pretty much nothing to do with any text substitution - I just figured that much was clear from the working reduced case (and the non-working full case).
| [reply] |
|
|