argius note

プログラミング関連

リストから一意のリストを得る(uniqコマンドを作る+余談)

今日は久々にPerlが大活躍したので何か書きます。


やっぱりあらゆるUnix-like環境でデフォなのは強い。
そういえば、Perlには組み込みのuniqって関数は無いんですね。今までやったことが無くて気づかなかった。もちろんモジュールを使えば簡単にできますが、私のところではデフォ環境での場合が基本ですので...


例として、コマンドラインパラメータのリストを一意リスト(コレクション型で言うところのset)にするコマンド"uniq.pl"を作ります。
今回書いたスクリプトでは、「ユニークのリストをソートしてカンマ区切りにする」だったので、この仕様で行きます。区切りはスペースで。

# バージョン1
my %h = ();
for (@ARGV) {
  $h{$_} = 1;
}
my @uniq = sort keys %h;
print "@uniq\n";

最初に見つけたサイトのいくつかは、この方式でした。ハッシュにつめればハッシュのキーは一意になるので、ユニークが達成できます。これで全く問題ありませんが、Perlらしい簡潔さが足りないようです。



ここはmapを使いましょう。*1リストの1要素をハッシュの要素に変換します。
(ここまではソラでできた。map { ( $_, 0 ) } のように書いたけど。)

# バージョン2
my %h = map { $_ => 1 } @ARGV;
my @uniq = sort keys %h;
print "@uniq\n";

mapの中カッコの中は、ハッシュのための構文糖みたいなもので、Perlでは"=>"はカンマと同義なのでこう書けます。ただ特別なのは、以下の例のようにbare-wordで書いてもエラーや警告が出ないところ。

my %h = ( a => 1, b => 2 );

バージョン2のmapが返すのは、コマンドライン引数が"a b c"だとしたら以下と同じになります。

my %h = ( a => 1, b => 1, c => 1 );

ここまで来たら、一行で書けるような気がしますよね。しかしながら、Perlには文脈という概念があり、返却先の方によって結果が変わるという性質があります。例えば、リストをスカラーで評価すると、要素数が返されます。

print scalar (1, 2, 3); # => 3

そのため、単純に%hを展開するだけでは上手くいきません(コンパイルエラー)。



ここでは、無名ハッシュを使います。以下の例では、¥演算子によるハッシュのリファレンス($r1)を得る方法と、無名ハッシュ($r2)のリファレンスを得る方法を示しています。

my %h = ( a => 1, b => 1, c => 1 );
my $r1 = \%h;
print $r1, "\n"; # => HASH(0x100b1770)
my $r2 = {map { $_ => 1 } @ARGV};
print $r2, "\n"; # => HASH(0x100b20c0)

参照を%{}で囲むとハッシュの実体が取り出せるので、無名ハッシュと組み合わせて変数を使わずに出力してみます。

# バージョン3(ラスト)
printf "%s\n", join " ", sort keys %{{map { $_ => 1 } @ARGV}};

丸カッコが無くて済むようにprintfを使いました。以下のように実行します。(shebangとか実行権の話は割愛します。)

$ perl uniq.pl a b c d c dd c a b
a b c d dd
$ perl uniq.pl 1 2 22 1 2 3 3 2 1
1 2 22 3
$

これで完成です。
なお、このバージョンではソートは文字列の評価なので、数値だけの場合でも数の順にはなりません。でも、ソートコマンドのn,rオプションのような機能を追加するのもそんなに難しくないはずです。



主に下記サイトを参考にさせていただきました。モジュールを使う場合の例があります。
重複値の削除(Perlで書く-Masのページ)

*1:写像」のこと。最近ではこれを言語仕様で?使える「内包表記」というのが有名ですね。内包表記はPerlで言えばmap+grepを式で書けるような機能で、HaskellとかPythonなど、他にも関数言語では(たぶん)当たり前のようになっていますね。