List::MoreUtilsのuniq関数の実装が変わってた件

大分昔に書いたHashを使って配列をユニークにしようという記事のブコメにてList::MoreUtilsのuniq関数の実装が変わったというコメントを頂きました。

List::MoreUtils 0.25_02 では少々変更されていた。 http://cpansearch.perl.org/src/VPARSEVAL/List-MoreUtils-0.25_02/lib/List/MoreUtils.pm

はてなブックマーク - kitsのブックマーク / 2009年11月17日

さっそくどう変わったのか確認したところ、なんともアンビリーバボーな実装になってました。

# List::MoreUtils-0.22
sub uniq (@) {
    my %h;
    map { $h{$_}++ == 0 ? $_ : () } @_;
}

# List::MoreUtils-0.25_02
sub uniq (@) {
    my %h;
    my $ref = \1;
    map { $h{defined $_ ? $_ : $ref}++ == 0 ? $_ : () } @_;
}

新uniq関数では、なにやら「1」のリファレンスを取ってmap中にdefinedでチェックしたり色々処理が増えてます。

何がしたいのかというと旧uniq関数ではハッシュの特性上、未定義値が空文字と同義と見なされてしまい、別々の値としてユニークにすることが出来なかったのでそれに対応するために新uniq関数ではごにょごにょやっているわけなんですね。

実際に挙動を見てみましょう。

use Data::Dumper;

sub uniq_old (@) {
    my %h;
    map { $h{$_}++ == 0 ? $_ : () } @_;
}

sub uniq_new (@) {
    my %h;
    my $ref = \1;
    map { $h{defined $_ ? $_ : $ref}++ == 0 ? $_ : () } @_;
}

my @array = ('','',undef,undef);

print Dumper([uniq_old(@array)]);
print Dumper([uniq_new(@array)]);
$VAR1 = [
          ''
        ];
$VAR1 = [
          '',
          undef
        ];

旧uniq関数の場合、ユニーク結果が空文字だけになってしまってます。新uniq関数だと空文字と未定義値がちゃんと別々のものとしてユニークになってますね。


しかし、この新uniq関数なんですが、少し実装に問題があります。

まず1のリファレンスを持っておき、配列中に未定義値があった場合その1のリファレンスの値を使用してユニーク処理を行ってるわけですが、この時たまたま配列に1のリファレンスの値とまったく同じ文字列が入っていた場合、誤認識してしまいます。ややこしい!

# 配列にリファレンスの値っぽい普通の文字列が入ってる。
# これがもしuniq関数中の\1のリファレンスの文字列と被ってたら誤認識する。
my @a = (undef,'SCALAR(0x1bc77e0)');

print Dumper [uniq(@a)];
$VAR1 = [
          undef
        ];

と、このように「'SCALAR(0x1bc77e0)'」という文字列が消えてしまいます。もちろんこの文字列はたまたま被った場合の話なので環境によっては値が異なります。

これに対応するとなるとこんな感じになりますかね。

sub uniq (@) {
    my (%h,$h);
    map { (defined $_ ? $h{$_} : $h)++ == 0 ? $_ : () } @_;
}

my @a = (undef,'SCALAR(0x1bc77e0)');

print Dumper [uniq(@a)];
$VAR1 = [
          undef,
          'SCALAR(0x1bc77e0)'
        ];

ばっちりですね。

しっかし、何でも汎用的に作ろうとするとどんどん実装が重くなっていきますよね。

このuniq関数にしたって空文字と未定義値を区別したいなんてニーズが果たしてどれくらいあるのかわからないのに汎用的であるが故に実装しなければならないという。