Reputation: 4401
I wrote some code where I tried to make list members unique by converting the list to a hash and then using the keys of that hash as elements. This worked without complaint in Perl 5.18, but now fails in Perl 5.26:
@a = keys { map { $_ => undef } @a }
The error messages are
Experimental keys on scalar is now forbidden at ...
Type of arg 1 to keys must be hash or array (not anonymous hash ({})) at ...
I'd like to avoid creating a temporary hash variable for that purpose.
In perlfaq4 How can I remove duplicate elements from a list or array? there is a solution, but it uses a separate temporary variable:
my %hash = map { $_, 1 } @array;
# or a hash slice: @hash{ @array } = ();
# or a foreach: $hash{$_} = 1 foreach ( @array );
my @unique = keys %hash;
#...
my %seen = ();
my @unique = grep { ! $seen{ $_ }++ } @array;
So how should I re-write the original code for newer perl?
Upvotes: 2
Views: 133
Reputation: 66954
... but now fails ...
Just to add a trivial statement about it failing, often overlooked. Straight from docs for keys
keys HASH
keys ARRAY
...
There you go -- keys
takes only hash or array variables. Not references, scalars, lists -- no, just variables.
As for what to do instead, there are great answers here already. I prefer using a library with a good name, and uniq
from List::Util
is as standard as they come, since one can then tell from across the room what that code is intended to do, without nearly any cognitive engagement.
(Or write your own, if you're so inclined, creating variables as needed, and give it a nice name.)
Upvotes: 1
Reputation: 132896
There was a short interval where Perl would try to guess what to do when an array or hash operator had a reference argument. This was introduced in v5.14:
my @keys = keys $ref;
This turned out to be such a bad idea that this was removed in v5.24, past which you get that error. The perlexperiment lists the experimental features, when they were introduced, and when they were removed.
With diagnostics you get longer explanations of the problem:
#!/usr/bin/perl
use v5.26;
use diagnostics;
my @a = ( 1, 2, 3, 4, 5, 3, 2 );
@a = keys { map { $_ => undef } @a };
This outputs:
Experimental keys on scalar is now forbidden at ... line 7.
Type of arg 1 to keys must be hash or array (not anonymous hash ({})) at ... line 7, near "};"
Execution of ... aborted due to compilation errors (#1)
(F) An experimental feature added in Perl 5.14 allowed each, keys,
push, pop, shift, splice, unshift, and values to be called with a
scalar argument. This experiment is considered unsuccessful, and
has been removed. The postderef feature may meet your needs better.
Uncaught exception from user code:
Experimental keys on scalar is now forbidden at ... line 7.
Type of arg 1 to keys must be hash or array (not anonymous hash ({})) at ... line 7, near "};"
Execution of ... aborted due to compilation errors.
But really, who cares about creating a temporary variable? Even in the anonymous reference case you are trying to use, you're still creating the data structure. In the ugly solution you posted in the comment to your question, you create an anonymous subroutine so you don't have to use a temporary variable. That is insane; you're doing more work and much more complicated work because you have a non-technical aversion to the easy way.
The solution is to just get over it and use the temporary variable (or uniq
as @ikegami showed). If I need a temporary variable to get something done, I use the variable and move on in life to worry about something else.
Upvotes: 4
Reputation: 67910
I'd like to avoid creating a temporary hash variable for that purpose.
You are already creating a temporary hash variable for that purpose. You are just not giving it a name.
When I read this question, what comes to mind is that you have not considered using the lexical scope. This is a key feature of Perl. Your code from perlfaq:
my %hash = map { $_, 1 } @array;
# or a hash slice: @hash{ @array } = ();
# or a foreach: $hash{$_} = 1 foreach ( @array );
my @unique = keys %hash;
#...
my %seen = ();
my @unique = grep { ! $seen{ $_ }++ } @array;
...can easily be altered to remove the permanence of the "temporary" variable by placing the variable in a scope. Which coinscidentally is conceptually the same as using List::Util::uniq
, or your own solution with an unnamed hash that is discarded after use.
my @unique;
{
my %hash = map { $_, 1 } @array;
@unique = keys %hash;
}
# here %hash no longer exists
# ....
my @unique;
{
my %seen; # = () is redundant.
@unique = grep { ! $seen{ $_ }++ } @array;
}
# here %seen no longer exists
Since we declare variables inside a block { ... }
, after the block ends, they are no longer available. This is essentially what your original solution is trying to do.
This latter solution is the one that uniq
uses. It also has the benefit of preserving the order of the unique elements. In the former, the order is lost, since hashes are unordered.
Upvotes: 1
Reputation: 31020
On new enough Perls you can use postfix deref:
@a = keys { map { $_ => undef } @a }->%*
Older Perls will need an infix deref:
@a = keys %{{ map { $_ => undef } @a }}
Or you could use an anonymous sub to separate the generation from the deduplication:
@a = sub { keys %{{@_}} }->( map { $_ => undef } @a )
But all of this is unnecessary, because List::Util::uniq
has been in core Perl since 5.8, which was released almost 23 years ago.
Upvotes: 5