リスト処理に有用な関数群を収録している Perl モジュール List::Util はコアモジュールであり、原則的にどの Perl 環境でも同梱されています。
とはいえ、Perl のバージョンアップに伴って、同梱されている List::Util もバージョンアップしており、収録されている関数にも差異があります。2010年前後の古い Perl だと外部モジュール List::MoreUtils を別途インストールしないと出来なかったことも、2020年代の新しい Perl であれば List::Util のみで完結することもあります。
以下、Perl のバージョンと、それに同梱されている List::Util のバージョン、そしてその List::Util が提供する関数一覧を表にしてみました。なお Perl 5.8 より前のバージョンの Perl は省略しています。
Perl | List::Util | List::Util で使える関数一覧 |
---|---|---|
5.8.0 | 1.07_00 | first min max minstr maxstr reduce sum shuffle |
5.10.0 | 1.19 | first min max minstr maxstr reduce sum shuffle |
5.12.0 | 1.22 | first min max minstr maxstr reduce sum shuffle |
5.14.0 | 1.23 | first min max minstr maxstr reduce sum shuffle |
5.16.0 | 1.23 | first min max minstr maxstr reduce sum shuffle |
5.18.0 | 1.27 | first min max minstr maxstr reduce sum sum0 shuffle |
5.20.0 | 1.38 | all any first min max minstr maxstr none notall product reduce sum sum0 shuffle pairmap pairgrep pairfirst pairs pairkeys pairvalues |
5.22.0 | 1.41 | all any first min max minstr maxstr none notall product reduce sum sum0 shuffle pairmap pairgrep pairfirst pairs pairkeys pairvalues |
5.24.0 | 1.42_02 | all any first min max minstr maxstr none notall product reduce sum sum0 shuffle pairs unpairs pairkeys pairvalues pairmap pairgrep pairfirst |
5.26.0 | 1.46_02 | all any first min max minstr maxstr none notall product reduce sum sum0 shuffle uniq uniqnum uniqstr pairs unpairs pairkeys pairvalues pairmap pairgrep pairfirst |
5.28.0 | 1.50 | all any first min max minstr maxstr none notall product reduce sum sum0 shuffle uniq uniqnum uniqstr head tail pairs unpairs pairkeys pairvalues pairmap pairgrep pairfirst |
5.30.0 | 1.50 | all any first min max minstr maxstr none notall product reduce sum sum0 shuffle uniq uniqnum uniqstr head tail pairs unpairs pairkeys pairvalues pairmap pairgrep pairfirst |
5.32.0 | 1.55 | all any first min max minstr maxstr none notall product reduce reductions sum sum0 sample shuffle uniq uniqint uniqnum uniqstr head tail pairs unpairs pairkeys pairvalues pairmap pairgrep pairfirst |
5.34.0 | 1.55 | all any first min max minstr maxstr none notall product reduce reductions sum sum0 sample shuffle uniq uniqint uniqnum uniqstr head tail pairs unpairs pairkeys pairvalues pairmap pairgrep pairfirst |
5.36.0 | 1.62 | all any first min max minstr maxstr none notall product reduce reductions sum sum0 sample shuffle uniq uniqint uniqnum uniqstr zip zip_longest zip_shortest mesh mesh_longest mesh_shortest head tail pairs unpairs pairkeys pairvalues pairmap pairgrep pairfirst |
5.38.0 | 1.63 | all any first min max minstr maxstr none notall product reduce reductions sum sum0 sample shuffle uniq uniqint uniqnum uniqstr zip zip_longest zip_shortest mesh mesh_longest mesh_shortest head tail pairs unpairs pairkeys pairvalues pairmap pairgrep pairfirst |
5.40.0 | 1.63 | all any first min max minstr maxstr none notall product reduce reductions sum sum0 sample shuffle uniq uniqint uniqnum uniqstr zip zip_longest zip_shortest mesh mesh_longest mesh_shortest head tail pairs unpairs pairkeys pairvalues pairmap pairgrep pairfirst |
私の場合、よく uniq
が使いたくて List::MoreUtils を入れたり自分で uniq
を書いたりしていたのですが、Perl 5.26 以降だと List::Util が uniq
を提供していることが分かります。
各関数の説明
先ほどの表は Perl と List::Util のバージョンが切り口でしたが、各関数を切り口に、その簡単な説明と初出の Perl バージョンを表にしてみました。
なお、同様に Perl 5.8 より前のバージョンは省略しています。Perl 5.8 以前に同梱されていた List::Util で使えた関数も、初出バージョンは 5.8 としています。
- 概ね第1引数はリストです
- 解説がない場合はリストです
- 第1引数の前、間接オブジェクトスロットに無名ブロックまたはサブルーチンリファレンスを取る
sort
型の関数について- いわゆる
foo { $a <=> $b } @list
やbar \&hint @list
といった呼び出し方をされる関数です - 解説では必要に応じて単に「(与えられた)ブロック」と書いています
- 説明の冒頭に記号注釈を書いています
- ブロックが1つの引数
$_
を取るものは&($_)
- ブロックが2つの引数
$a
および$b
を取るものは&($a, $b)
- ブロックが1つの引数
- ブロックが引数
$_
を取るものは、リストの左側から順番に要素を$_
に代入、そのブロックにある最後に評価された文の評価結果に応じて何かが行われます
- いわゆる
- reduce はセルに入れるには説明が複雑になったので省いていますが、多くのプログラミング言語にある
reduce
と同様です- (I) 最初に呼び出されるブロックでは
($a, $b) = @list[0,1]
とする - (II)
$a
$b
におけるブロックの評価値を改めて$a
とする - (III)
$list[2]
を$b
とする - (IV) II を行う
- (V)
$list[$i]
の添字を1増分して III を行う - (VI) 添字がリストの要素数を超過する直前まで上記 IV〜V を繰り返す
- (VII) 最後の
$a
の結果がreduce
全体の評価結果となる
- (I) 最初に呼び出されるブロックでは
- reductions は reduce の処理途中を見せてくれる版です
- 例として
-
say join ",", reductions { $a+$b } 1..10
を実行すると1,3,6,10,15,21,28,36,45,55
を表示します -
say join ",", reductions { $a . "-" . $b } 1..5
を実行すると1,1-2,1-2-3,1-2-3-4,1-2-3-4-5
を表示します
-
-
reductions
が返す最後の要素が、同じ引数でreduce
を実行したときの結果です
- 例として
- uniq 系について
- uniq は通常の重複を省きます。重複判定は
eq
で行われると考えるとわかりやすいです - uniqnum は uniq の数値評価版です。重複判定は
==
で行われると考えるとわかりやすいです - uniqint は uniq の整数評価版です。重複判定は
int
したものについて==
で行われると考えるとわかりやすいです -
@elements = (111, 222, 333, "222.0", "333.1")
を比較すると違いが分かりますuniq(@elements); # => 111|222|333|222.0|333.1
uniqnum(@elements); # => 111|222|333|333.1
uniqint(@elements); # => 111|222|333
- uniq は通常の重複を省きます。重複判定は
- zip mesh 系について
- zip はいわゆる転置行列を作るものだと考えるとわかりやすいかもしれません
- mesh は zip と似ていますが、返り値はフラットなリストとなります
- mesh の返り値のリストは、同じ引数の zip の返り値のリストを
map { @$_ }
したものと考えて良いでしょう
- mesh の返り値のリストは、同じ引数の zip の返り値のリストを
- 2個の配列リファレンスを引数に取る mesh の返り値をハッシュに代入する使い方が有効でしょう
my %hash = mesh \@keys, \@values;
- ペア系関数については詳細な解説を省いています
- 今後の記事更新で追加するかもしれません
- 詳細は perldoc(英語、日本語)を参照下さい
関数 | 説明 | 初導入 Perl バージョン |
---|---|---|
first |
&($_) ブロックの評価結果が真となる最初の引数リストの要素を返す |
5.8 |
min | リストにある数の最小値を返す(数値評価) | 5.8 |
max | リストにある数の最大値を返す(数値評価) | 5.8 |
minstr | リストにある文字列の辞書順で最も最初のものを返す(文字列評価) | 5.8 |
maxstr | リストにある文字列の辞書順で最も最後のものを返す(文字列評価) | 5.8 |
reduce |
&($a, $b) (別記) |
5.8 |
sum | リストの合計値(総和)を返す(数値評価) | 5.8 |
shuffle | リストの順序をランダムに入れ替えた新しいリストを返す | 5.8 |
sum0 |
sum とほぼ同じ。ただし、引数リストが空だった場合、 sum は undef を返すが、sum0 は 0 を返す |
5.18 |
all |
&($_) ブロックの評価結果が全ての引数リストの要素で真の場合、真を返す |
5.20 |
any |
&($_) ブロックの評価結果が何らかの引数リストの要素で真の場合、真を返す |
5.20 |
none |
&($_) ブロックの評価結果が全ての引数リストの要素で偽の場合、真を返す |
5.20 |
notall |
&($_) none の別名 |
5.20 |
product | リストの全ての積の結果(総乗)を返す | 5.20 |
pairmap |
&($a, $b) map のペアバージョン |
5.20 |
pairgrep |
&($a, $b) grep のペアバージョン |
5.20 |
pairfirst |
&($a, $b) first のペアバージョン |
5.20 |
pairs | 各要素がペア配列リファレンスであるリストを返す | 5.20 |
pairkeys | 与えられたリストをペアリストとみなし、偶数添字の要素のみ抜き出したリストを返す | 5.20 |
pairvalues | 与えられたリストをペアリストとみなし、奇数添字の要素のみ抜き出したリストを返す | 5.20 |
unpairs |
pairgrep などで得られる、各要素がペア配列リファレンスであるリストの配列リファレンスをデリファレンスしたリストを返す |
5.24 |
uniq | 重複する要素を省いたリストを返す | 5.26 |
uniqnum | 数値として評価した結果、重複する要素を省いたリストを返す | 5.26 |
uniqstr | 文字列として評価した結果、重複する要素を省いたリストを返す | 5.26 |
head |
head $size, @list の形で呼び出され、最大 $size 個で元のリストを左側に切り詰めた新しいリストを返す |
5.28 |
tail |
tail $size, @list の形で呼び出され、最大 $size 個で元のリストを右側に切り詰めた新しいリストを返す |
5.28 |
reductions |
&($a, $b) ( reduce と合わせて別記) |
5.32 |
sample |
sample $count, @list の形で呼び出され、 @list からランダムに $count 個の要素を取り出した新しいリストを返す。 (shuffle @list)[0..($count-1)] と同等。 |
5.32 |
uniqint | 整数として評価した結果、重複する要素を省いたリストを返す | 5.32 |
zip | 配列リファレンスのリストを取って、転置となる配列リファレンスのリストを返す | 5.36 |
zip_longest | zip の別名 | 5.36 |
zip_shortest | zip と同様だが、引数の中で最も要素数が少ない配列リファレンスに処理を合わせる( undef を埋めない) |
5.36 |
mesh | zip と同様の引数を取るが、zip の返り値となる配列リファレンスを展開したリストを返す | 5.36 |
mesh_longest | mesh の別名 | 5.36 |
mesh_shortest | mesh と同様だが、引数の中で最も要素数が少ない配列リファレンスに処理を合わせる(undef を埋めない) |
5.36 |
List::Util に無ければ List::MoreUtils を探すサンプル
前述の uniq
のように、古くは List::MoreUtils を使っていたものがコアモジュールの List::Util で使えるようになりました。
「古い perl のために List::MoreUtils から uniq
関数をインポートするコードも残したいけれど、新しい perl では List::Util から uniq
関数をインポートしたい(あえて List::MoreUtils をインストールしたくない)」という場合、例えば以下のように書くと良いでしょう。
# use constant DEBUG => $ENV{DEBUG};
BEGIN {
local $@;
eval {
require List::Util;
import List::Util qw(uniq);
#print "success load List::Util::uniq\n" if DEBUG;
};
return if !$@;
undef $@;
eval {
require List::MoreUtils;
import List::MoreUtils qw(uniq);
#print "success load List::MoreUtils::uniq\n" if DEBUG;
};
die $@ if $@
}
import List::Util qw(uniq)
は間接オブジェクト記法 (indirect object syntax) という書き方。 require
と並列すると縦に揃うのでこういうときに限って使われたりしますが、一般的には混乱が多い記法ということで Perl 5.32 から Perl 7 にかけて明示的に非推奨になっていく動きのようです。将来を見据えるなら以下のように間接オブジェクト記法を使わない書き方が安心かも。
# use constant DEBUG => $ENV{DEBUG};
BEGIN {
local $@;
eval {
require List::Util;
List::Util->import(qw(uniq));
#print "success load List::Util::uniq\n" if DEBUG;
};
return if !$@;
undef $@;
eval {
require List::MoreUtils;
List::MoreUtils->import(qw(uniq));
#print "success load List::MoreUtils::uniq\n" if DEBUG;
};
die $@ if $@;
}
なお、難読にはなりますが、上記を for
ループを使ってさらに短く書くとすると以下のようになります。require "List::Util"
と require
に文字列を与えると、字面通りの "List::Util"
という名前のファイルを @INC
から探そうとするので、裸のワードが渡るよう require List::Util
として実行するため、例外補足のためのブロック eval
から文字列 eval
に代えています。
BEGIN {
my $e;
for my $module (qw(List::Util List::MoreUtils)) {
local $@;
eval qq{require $module; import $module qw(uniq);};
return if !($e = $@)
}
die $e if $e;
}
余談:上記の表の出し方
上記の表、最初は手で作成しようと思っていたのですが、以下の方針で多少の自動化をしてみました。
- List::Util の Git リポジトリを clone する
- 特定の Perl バージョンが同梱している List::Util のバージョンを corelist コマンドで調査する
- リポジトリ内に lib/List/Util.pm が存在し、そこの
@EXPORT_OK
を見ると関数一覧がある - リポジトリには List::Util のバージョン番号に対応したタグが大体打たれているので、知りたいバージョンに対応したタグで checkout して前述の
@EXPORT_OK
を抜き出す
List::Util の GitHub リポジトリ を clone、リポジトリの作業ディレクトリトップに移動して、以下のような雑に書いたプログラムを保存、そして実行しました。
#!/usr/bin/env perl
use strict;
use warnings;
use feature qw(say signatures);
no warnings qw(experimental::signatures);
# vX.Y.Z
use constant VERSION_Y_MIN => 8;
use constant VERSION_Y_MAX => ($^V =~ /^v5\.(\d+)/);
die if !-f "lib/List/Util.pm";
my %special_vtag_mapping = (
"v1.07_00" => "v1.07",
"v1.42_02" => "v1.42_002",
"v1.46_02" => "v1.46",
);
my @versions = list_util_versions();
# say join "\t", @$_ for @versions;
# say "---";
for my $pair_version (@versions) {
my ($perl_version, $list_util_version) = @$pair_version;
my $vtag = "v$list_util_version";
if ( local $_ = $special_vtag_mapping{$vtag} ) {
$vtag = $_;
}
git_checkout($vtag);
my @methods = list_util_methods();
printf "%s\t%s\t%s\n",
$perl_version, $list_util_version, join " ", @methods;
}
sub git_checkout($arg) {
my $cmd = "git checkout $arg >> debug.log 2>&1";
my $rv = system $cmd;
if ( $rv > 0 ) {
die "command error ($rv): $cmd";
}
}
sub list_util_versions {
my @y_versions = (VERSION_Y_MIN .. VERSION_Y_MAX);
my @versions; # [PERL_VERSION, LU_VERSION], ...
for my $y ( grep { $_ % 2 == 0 } @y_versions ) {
my $perl_version = "5.$y.0";
my $output = capture( 'corelist', '-v', $perl_version, 'List::Util' );
#printf "%s\t%s\n", $perl_version, $output;
my ($list_util_version) = $output =~ /(\S+)$/;
push @versions, [$perl_version, $list_util_version];
}
return @versions;
}
sub list_util_methods {
my $content = slurp("lib/List/Util.pm");
$content =~ m{
\@EXPORT_OK
\s* = \s*
qw\( (?<exports>.*?) \)
}sx;
my $exports = $+{exports} || die;
my @methods = $exports =~ /(\S+)/g;
return @methods;
}
sub slurp($filename) {
open my $fh, '<', $filename or die;
my @content = <$fh>;
close $fh;
return join "", @content;
}
sub capture (@command) {
my $pid = open my $pipe, '-|', @command;
my @output = <$pipe>;
chomp @output;
close $pipe;
return wantarray ? @output : join "\n", @output;
}
Markdown のテーブルにするため縦棒を入れたいなと
$ perl list-util.pl | perl -pe 's/\t/ | /g; s/^/| /; s/$/ |/'
という風に実行、そして記事中に貼り付けてテーブル見出しを書き加えたりしました。たぶん年1回の記事更新時にも使えるかな。
なお、プログラム中で $^V
を参照している通り、現在の perl が調べたい最新バージョンとなっている必要があります。plenv 等でビルドしておきましょう。