in reply to Ternary Operator: Condition-Negation

When I first saw this post I was struck by the non-perlishness of the following loop...

# eliminate duplicates in history and keep only the most recent for(my $i=scalar(@history)-1;$i>=0;$i--) { $_ = $history[$i]; !$history{$_} ? $history{$_}=1 : splice(@history,$i--,1); }

...and thought I'd have a crack at "improving it".

What I arrived at after going all round the houses was:

{ my (%history, $histcount); # Should I initialise $histcount = 0 or + is that "Cargo cult"? sub addHist { $history{ $_[0] } = $histcount++; } sub getHist { return = grep{ $history{ $_ } == ($histcount - $_[0]); + } keys %history; } } ... # get $newvalue from somewhere addHist( $newvalue ); ... # deside we want the $n'th previous value my $last_nth = getHist( $n ); ...

If anyone is interested in seeing the route by which I arrived at this, or fancies answering a couple of question arising...

First, understand what it is doing. (Excessive comments are just for my annotation)

for ( my $i=$#history; $i >=0; $i-- ) { # for $i = (last index of @hi +story) to 0 $_ = $history[$i]; # get the value from the posi +tion in @history !$history{$_} # test if that value has been + seen before ? $history{$_} = 1 # if it hasn't record it : splice @history, $i--, 1) # if it has remove it from @h +istory # (reducing $i to compensate +for its remove; }

Simple improvement?

for my $i ($#history .. 0 ) { !exists $history{ history[$i] } # use exists as testing its v +alue against zero will # autovivify (which is why we + need to set its value to 1) ? $history{ $history[$i] }++ # autovivify and increment. ( +By using exists, the # setting to 1 is redundant b +ut... : splice @history, $i--, 1); # the -- in this version woul +d be bad!! # Thou shalt not modify the i +terator in a for loop! }

Try again

for ($#history .. 0) { my $temp = pop @history; if (!exists $history{ $temp }) { $history{ $temp }++; unshift @history; } # else nothing to do we've already removed it. }

Simplify?

for ($#history .. 0) { my $temp = pop @history; next if exists $history{ $temp }; $history{ $temp } ++; unshift @history; }

still bugged by that temp var! So...

for ( push @history, '~~SOME UNLIKELY VALUE~~', my $value= shift @hist +ory; $value ne '~~SOME UNLIKELY VALUE~~'; $value = shift @history ) { push @history, $value, $history{ $value }++ unless exists $history +{ $value } }

Of course, that duplication of the sentinel value is bad so

use constant SENTINEL => '~~SOME UNLIKELY VALUE~~'; for ( push @history, SENTINEL, my $value= shift @history; $value ne SENTINEL; $value = shift @history ) { push @history, $value, $history{ $value }++ unless exists $history +{ $value } }

But all I've really done is to disguise the temp var and I'm back to using the clunky for(;;) loop:(

Then I thought. Ah, the problem is allowing duplicates to exist in the first place! That is also what is forcing the need for a loop and a temporary hash to eliminate them! Why use an array to store them anyway, why not just store the history in the hash?

my %history; .... # get a new value here and only add it if its not there. $history{ $newvalue } = 1 unless exists $history{ $newvalue }; ...

That seemed great until I tried to recover the last value - returns from keys %hash aren't ordered. Drat!

Ah! But that value 1 isn't doing much of anything useful. Ok!So I'll maintain a count.

my (%history, $histcount); .... # get a new value here and only add it if its not there. $history{ $newvalue } = $histcount++ unless exists $history{ $newvalue + };

but I don't need the conditional, cos if it already exists, we want to promote it to the top of the list. Just need...

$history{ $newvalue } = $histcount++; ...

That's simple, promotes and eliminates dups! Then to retrieve the nth previous value

There's no direct way to go from a hash value back to its key so we need a loop.

for my $i (keys %history) { last if $history{ $i } == ($histcount - $n); } my $last_nth = $history{ $i };

Doesn't work cos $i is local (Sorry! Lexically scoped) to the for(){}. Its a shame that none of these work either

# Can't use multiple modifiers. Basic Plus2 was nice! my last_nth = do { $_ for (keys %history) } while ( $history{ $_ } == +($histcount - $n) ); my $last_nth = $_ if $history{ $_ } == n for (keys %history); # Again +$_ is local

Could do

my $last_nth; for my $i (keys %history) { last if ($last_nth = $history{ $i }) == ($histcount - $n); }

Should work ok, but seems clunky. I've traded an array, a temporary hash and a loop with a temp var when adding for a hash and a loop with a temp var when retrieving. Maybe map or grep would help?

my last_nth = grep{ $history{ $_ } == ($histcount - $n } keys %history +;

Tells me its there, but not what or where

my ($last_nth) = grep{ $history{ $_ } == ($histcount - $n); } keys %hi +story;

Finally. But is this "good code"?

Eliminating all the false starts and clunky code this translates into

my (%history, $histcount); # To add $newvalue $history{ $newvalue } = $histcount++ unless exists $history{ $newvalue + }; # To retrieve the last-but-$n'th my ($last_nth) = grep{ $history{ $_ } == ($histcount - $n); } keys %hi +story;

Not too bad, but it would probably be better if the above where subs addHist() and getHist() but they would either have to access %history and $histcount from external to themselves

my (%history, $histcount); # Should I initialise $histcount = 0 or + is that "Cargo cult"? sub addHist { $history{ $_[0] } = $histcount++; } sub getHist { return = grep{ $history{ $_ } == ($histcount - $_[0]); } + keys %history; } # then ... # get $newvalue from somewhere addHist( $newvalue ); ... # deside we want the $n'th previous value my $last_nth = getHist( $n ); ...

...or these would have to be passed on each call

my (%history, $histcount); # Should I initialise $histcount = 0 or + is that "Cargo cult"? sub addHist { my ($hashref, $histcount, $new ) = @_; # Naming our parameters ma +kes things clearer # validate $hashref->{ $new } = $histcount++; } sub getHist { my ($hashref, $histcount, $new ) = @_; #validate return = grep{ $hashref->{ $_ } == ($histcount - $n); } keys %hist +ory; }

then

... # get $newvalue from somewhere addHist( \%history, $histcount, $newvalue ); ... # deside we want the $n'th previous value my $last_nth = getHist( \%history, $histcount, $n ); ...

Better, but in terms of a single program using a single history list, this seems like a step backwards. Maybe this is a good time to think closures?

{ my (%history, $histcount); # Should I initialise $histcount = +0 or is that "Cargo cult"? sub addHist { $history{ $_[0] } = $histcount++; } sub getHist { return = grep{ $history{ $_ } == ($histcount - $_[0] +); } keys %history; } }

then as before, but nobody can mess with our hash or its pointer accidently!

... # get $newvalue from somewhere addHist( $newvalue ); ... # deside we want the $n'th previous value my $last_nth = getHist( $n ); ...

Now the questions arising.

Is one of these solutions better than another?

If the last one is good, is it worth putting this into a seperate package using OO the thing?

Is there a better way?


What's this about a "crooked mitre"? I'm good at woodwork!

Replies are listed 'Best First'.
Re(2): Ternary Operator: Condition-Negation
by Arien (Pilgrim) on Aug 28, 2002 at 23:23 UTC
    Simplify?
    for ($#history .. 0) { my $temp = pop @history; next if exists $history{ $temp }; $history{ $temp } ++; unshift @history; }

    Why not follow through when you get to that point? Something like this seems just fine to me:

    my %seen; for (1 .. @history) { my $item = pop @history; unshift @history, $item unless $seen{$item}++; }

    Sure, the temporary variable is still there. But then, do you really prefer something along these lines?

    my %seen; for (1 .. @history) { unshift @history, $seen{$history[-1]}++ ? $#history-- && () : pop +@history; }

    I didn't think so either. ;-)

    — Arien