Mobile Factory Tech Blog

技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします!

PerlのList::AllUtilsの逆引き

この記事はモバイルファクトリー Advent Calendar 2020 16日目の記事です。

はじめに

こんにちは、エンジニアの id:Dozi0116 です。 自分は4月に入社してから現在まででいろいろなPerlのコードに触れられたのですが、その中で List::AllUtils というモジュールが印象に残っています。

List::AllUtilsとは、「リスト操作のいろいろが詰まったモジュール」を集めて使えるようにしたモジュールで、

の3つのモジュールが一体化したモジュールです。

実際の開発でも使っていますが、たくさんの関数が詰まっているため、どんなことができるのか?を全く理解しきれていません。また、既に書かれているソースコードをすぐにList::AllUtilsのものと理解ができず、新しい関数を見かけるたびにこんなことまでできるのか!と驚いていました。

せっかくこんな便利モジュールを使っているのに使いこなせないのはもったいない… そこで、List::AllUtilsができることを理解するため、やりたいことから使うべきコードを見つけやすくなるように、そして他に同じように困っている人のハードルを下げるため、この逆引きを作りました。

動作環境

Perl 5.30.2
List::AllUtils 0.18
List::SomeUtils 0.56
List::Util 1.45
List::UtilsBy 0.11

逆引き

今回載せている例はこのテストコードで検証しています。

今回の逆引きでは、reduceなどの発想次第でなんでもできそうな関数は、既にある関数と差別化ができる場合にのみ載せています。ご了承ください。 また、List::AllUtilsの依存バージョン関係上、使えない関数がいくつかあります。

条件を満たす最初の要素をとる

@list = (4, 7, 1);
$result = first { $_ > 5 } @list; # 7
$result = first_value { $_ > 5 } @list; # 7
$result = firstval { $_ > 5 } @list; # first_valueのエイリアス

extract_first_by は条件を満たした要素をオリジナルのリストから消す

@list = (4, 7, 1);
$result = extract_first_by { $_ > 5 } @list; # 7
print(@list); # (4, 1);

reduce で書くこともできる

@list = (4, 7, 1);
$result = reduce {
    defined($a)         ? $a :
    $b > 5                 ? $b :
                                undef
} undef, @list;

条件を満たす最後の要素をとる

@list = (4, 7, 1);
$result = last_value { $_ < 5 } @list; # 1
$result = lastval { $_ < 5 } @list; # 同じ(last_valueのエイリアス)

条件を満たす要素より後ろの要素たちを抽出する

@list = (2, 4, 6, 8, 10);
@result = after { $_ > 5 } @list; # (8, 10)

条件を満たす要素以降の要素たちを抽出する

@list = (2, 4, 6, 8, 10);
@result = after_incl { $_ > 5 } @list; # (6, 8, 10)

条件を満たす要素より前の要素たちを抽出する

@list = (2, 4, 6, 8, 10);
@result = before { $_ > 5 } @list; # (2, 4)

条件を満たす要素以前の要素たちを抽出する

@list = (2, 4, 6, 8, 10);
@result = before_incl { $_ > 5 } @list; # (2, 4, 6)

先頭からいくつかの要素を抽出する

@list = 1..10;
@result = head 3, @list; # (1, 2, 3)
@result = head -2, @list; # (1, 2, 3, 4, 5, 6, 7, 8)

末尾からいくつかの要素を抽出する

@list = 1..10;
@result = tail 3, @list; # (8, 9, 10)
@result = tail -2, @list; # (3, 4, 5, 6, 7, 8, 9, 10)

1つしかない要素のみを抽出する

@list = (1, 1, 1, 2, 3, 3, 4, 5);
@result = singleton @list; # (2, 4, 5)

条件を満たす要素を検索する

@list = 1...10;
@result = grep { $_ == 4 } @list; # (4)

二分探査をするため、 CODE BLOCKは 比較した要素が小さいなら-1を、大きいなら1を、ちょうどなら0を返す必要が、また@listはソートされている必要がある

@sorted_list = 1...10;
@result = bsearch { $_ <=> 4 } @sorted_list; # (4)

extract_byは見つけた要素をresultに抜き出して、オリジナルから消える

@list = 1...10;
@result = extract_by { $_ == 4 } @list; # (4)
print(@list); # (1, 2, 3, 5, 6, 7, 8, 9, 10)

条件を満たす要素数を求める

@list = 1...10;
$result = true { $_ < 4 } @list; # 3

条件を満たさない要素数を求める

@list = 1...10;
$result = false { $_ < 4 } @list; # 6

条件を満たす要素のインデックスを求める

二分探査をするため、 CODE BLOCKは 比較した要素が小さいなら-1を、大きいなら1を、ちょうどなら0を返す必要が、また@listはソートされている必要がある

@sorted_list = 1...10;
$result = bsearch_index { $_ <=> 4 } @sorted_list; # 3
$result_b = bsearchidx { $_ <=> 4 } @sorted_list; # 同じ(bsearch_indexのエイリアス)

複数のインデックスをまとめて求めるなら indexesを使う

@list = (1, 1, 1, 2, 4, 1);
@result = indexes { $_ == 1 } @list; # (0, 1, 2, 5)

条件を満たす最初の要素のインデックスを求める

@list = (1, 1, 1, 2, 4, 1);
$result = first_index { $_ == 1 } @list; # 0
$result = firstidx { $_ == 1 } @list; # 同じ(first_indexのエイリアス)

条件を満たす最後の要素のインデックスを求める

@list = (1, 1, 1, 2, 4, 1);
$result = last_index { $_ == 1 } @list; # 5
$result = lastidx { $_ == 1 } @list; # 同じ(last_indexのエイリアス)

条件を満たすただ一つの要素のインデックスを求める

要素が複数あった場合、 -1 が返る

@list = (1, 1, 1, 2, 4, 1);
$result = only_index { $_ == 2 } @list; # 3
$result = onlyidx { $_ == 2 } @list; # 同じ(only_indexのエイリアス)

$result = only_index { $_ == 1 } @list; # -1

最初にCODE BLOCKが正常終了する要素の結果を求める

@list = (4, 7, 1);
$result = first_result { $_ ** 2 if $_ > 3 } @list; # 16
$result = firstres { $_ ** 2 if $_ > 3 } @list; # 同じ(first_indexのエイリアス)

最後にCODE BLOCKが正常終了する要素の結果を求める

@list = (4, 7, 1);
$result = last_result { $_ ** 2 if $_ > 3 } @list; # 49
$result = lastres { $_ ** 2 if $_ > 3 } @list; # 同じ(last_indexのエイリアス)

一つだけCODE BLOCKが正常終了する要素の結果を求める

@list = (4, 7, 1);
$result = only_result { $_ ** 2 if $_ > 5 } @list; # 49
$result = onlyres { $_ ** 2 if $_ > 5 } @list; # 同じ(last_indexのエイリアス)

# 正常終了する要素が複数ある場合、undefを返す
$result = only_result { $_ ** 2 if $_ > 3 } @list; # undef

要素の最大値を求める

@list = (1, 4, 3);
$result = max @list; # 4

max_by で書けば、CODE BLOCKは自由に記述できるので器用なことができる

@list = ({ value => 2, id => 1 }, { value => 5, id => 2 }, { value => 5, id => 3 });
$result = max_by { $_->{value} } @list; # { value => 5, id => 2 }

リストコンテキストを返り値に期待すれば、全部の要素を取得できる

@list = ({ value => 2, id => 1 }, { value => 5, id => 2 }, { value => 5, id => 3 });
@result = max_by { $_->{value} } @list; # ({ value => 5, id => 2 }, { value => 5, id => 3 })

文字列の最大を求める

ここでいう文字列の最大とは、文字コード比較での最大を指す

@list = qw/a b c/;
$result = maxstr @list; # c
@list = ({ name => 'a' }, { name => 'b' }, {name => 'c' });
$result = reduce { $a->{name} gt $b->{name} ? $a : $b } @list; # { name => 'c' }

要素の最小値を求める

@list = (2, 3, 1);
$result = min @list; # 1

min_by で書けば、CODE BLOCKは自由に記述できるので器用なことができる

@list = ({ value => 2, id => 1 }, { value => 1, id => 2 }, { value => 1, id => 3 });
$result = min_by { $_->{value} } @list; # { value => 1, id => 2 }

リストコンテキストを返り値に期待すれば、全部の要素を取得できる

@list = ({ value => 2, id => 1 }, { value => 1, id => 2 }, { value => 1, id => 3 });
@result = min_by { $_->{value} } @list; # ({ value => 1, id => 2 }, { value => 1, id => 3 })

文字列の最小を求める

ここでいう文字列の最小とは、文字コード比較での最小を指す

@list = qw/b c a/;
$result = minstr @list; # a
@list = ({ name => 'b' }, { name => 'a' }, {name => 'c' });
$result = reduce { $a->{name} lt $b->{name} ? $a : $b } @list; # { name => 'a' }

最小値と最大値を同時に求める

@list = (2, 3, 1);
($min,$max) = minmax @list; # (1, 3)

minmax_by で書けば、CODE BLOCKは自由に記述できるので器用なことができる

@list = ({ value => 2, id => 1 }, { value => 1, id => 2 }, { value => 1, id => 3 });
($min, $max) = minmax_by { $_->{value} } @list; # ({ value => 1, id => 2 }, { value => 2, id => 1 })

リストを文字列順で並び替える

@list = ('banana', 'melon', 'apple');
@result = sort @list; # ('apple', 'banana', 'melon')

比較するものが組み込み関数のsortと違って省略できるため、sort_byを使えば比較的簡潔に書くことができる

@list = ({ name => 'banana' }, { name => 'melon' }, { name => 'apple' });
@result = sort_by { $_->{name} } @list; # ({ name => 'apple' }, { name => 'banana' }, { name => 'melon' })

sort_by を降順で使いたい時は rev_sort_by が使える

@list = ({ name => 'banana' }, { name => 'melon' }, { name => 'apple' });
@result = rev_sort_by { $_->{name} } @list; # ({ name => 'melon' }, { name => 'banana' }, { name => 'apple' })

リストを数値順で並び替える

@list = (23, 1, 12);
@result = sort { $a <=> $b } @list; # (1, 12, 23)

比較するものが組み込み関数のsortと違って省略できるため、nsort_byを使えば比較的簡潔に書くことができる

@list = ({ value => 23 }, { value => 1 }, { value => 12 });
@result = nsort_by { $_->{value} } @list; # ({ value => 1 }, { value => 12 }, { value => 23 })

nsort_by を降順で使いたい時は rev_nsort_by が使える

@list = ({ value => 23 }, { value => 1 }, { value => 12 });
@result = rev_nsort_by { $_->{value} } @list; # ({ value => 23 }, { value => 12 }, { value => 1 })

文字列を結合する

@list = qw/a b c/;
$result = join '', @list; # "abc"

reduce で書けば、CODE BLOCKは自由に記述できるので器用なことができる

@list = qw/a b c/;
$result = reduce { uc($a) . uc($b) } @list; # "ABC"

要素の合計を求める

@list = 1..10;
$result = sum @list; # 55

# 要素がない時はundefを返す
@list = ();
$result = sum @list; # undef

sum0を用いると要素0のリストの場合は0を返す

@list = ();
$result = sum0 @list; # 0
@list = ({ value => 2 }, { value => 4 }, { value => 1 });
$result = reduce { $a + $b->{value} } 0, @list; # 7

要素の積を求める

@list = 1..10;
$result = product @list; # 3628800

# 要素がない時は1を返す
@list = ();
$result = product @list; # 1
@list = ({ value => 2 }, { value => 4 }, { value => 1 });
$result = reduce { $a * $b->{value} } 1, @list; # 8

各要素のうちどれかが条件を満たすならtrue

@list = ({ flag => 1 }, { flag => 0 }, { flag => 1 });
$result = any { $_->{flag} } @list; # 1

# 空リストの場合、falseを返す
@list = ();
$result = any { $_->{flag} } @list; # ""

any_u は空リストの場合にundefを返す

@list = ();
$result = any { $_->{flag} } @list; # ""
$result = any_u { $_->{flag} } @list; # undef

各要素のうちどれかが条件を満たさないならtrue

@list = ({ flag => 1 }, { flag => 0 }, { flag => 1 });
$result = notall { $_->{flag} } @list; # 1

# 空リストの場合、falseを返す
@list = ();
$result = notall { $_->{flag} } @list; # ""

notall_u は空リストの場合にundefを返す

@list = ();
$result = notall { $_->{flag} } @list; # ""
$result = notall_u { $_->{flag} } @list; # undef

全要素が条件を満たすならtrue

@list = ({ flag => 1 }, { flag => 1 }, { flag => 1 });
$result = all { $_->{flag} } @list; # 1

# 空リストの場合、trueを返す
@list = ();
$result = all { $_->{flag} } @list; # 1

all_u は空リストの場合にundefを返す

@list = ();
$result = all { $_->{flag} } @list; # 1
$result = all_u { $_->{flag} } @list; # undef

全要素が条件を満たさないならtrue

@list = ({ flag => 0 }, { flag => 0 }, { flag => 0 });
$result = none { $_->{flag} } @list; # 1

# 空リストの場合、trueを返す
@list = ();
$result = none { $_->{flag} } @list; # 1

none_u は空リストの場合にundefを返す

@list = ();
$result = none { $_->{flag} } @list; # 1
$result = none_u { $_->{flag} } @list; # undef

要素がただ1つだけ条件を満たすならtrue

@list = ({ flag => 0 }, { flag => 1 }, { flag => 0 });
$result = one { $_->{flag} } @list; # 1

# 空リストの場合、falseを返す
@list = ();
$result = one { $_->{flag} } @list; # ""

one_u は空リストの場合にundefを返す

@list = ();
$result = one { $_->{flag} } @list; # ""
$result = one_u { $_->{flag} } @list; # undef

それぞれの要素に処理を行いたい

@list = (4, 7, 1);
@result = map { $_ *= 2 } @list; # (8, 14, 2)
print(@list); # (8, 14, 2);

apply で書けば、元のリストは変更されない

@list = (4, 7, 1);
@result = apply { $_ *= 2 } @list; # (8, 14, 2)
print(@result); # (4, 7, 1)

bundle_byで書けば、複数の要素をまとめて処理できる

@list = (1...8);
@result = bundle_by { [$_[0], $_[1], $_[2]] } 3, @list; # ([1, 2, 3], [4, 5, 6], [7, 8, undef])

要素をランダムに並び替える

@list = ('a', 'b', 'c', 'd');
@result = shuffle @list; # 何が出るかは神のみぞ知る

weighted_shuffle_byで書けば、重みをつけたランダムになる

@list = ('a', 'b', 'c');
@result = weighted_shuffle_by { { a => 1, b => 0, c => 99 }->{$_} } @list; # ほぼほぼ ('c', 'a', 'b')

最頻値を求める

@list = ('apple', 'pineapple', 'apple', 'banana', 'apple', 'apple', 'apple');
@result = mode @list; # ('apple')

重複を弾く

後に出てきた重複要素が消される

@list = ('hoge', 'hoge', 22, 35, 10, 22);
@result = uniq @list; # ('hoge', 22, 35, 10)
@result = distinct @list; # 同じ(uniqのエイリアス)

要素が数値or文字列で一定なら以下の関数も使える

@list = (1, 2, 3, 1, 5);
@result = uniqnum @list; # (1, 2, 3, 5)
@list = ('a', 'A', 'aa', 'a');
@result = uniqstr @list; # ('a', 'A', 'aa')

uniq_by で書けば、CODE BLOCKは自由に記述できるので器用なことができる

@list = ({ value => 2, id => 1 }, { value => 1, id => 2 }, { value => 1, id => 3 });
@result = uniq_by { $_->{value} } @list; # ({ value => 2, id => 1 }, { value => 1, id => 2 })

特定の要素の後ろに要素を追加する

@list = (1, 2, 3, 5);
my $result = insert_after { $_ == 3 } 4, @list;
@list; # (1,2,3,4,5)

stringの等価比較をするなら、insert_after_stringが使える

@list = ('first', 'second', 'third', 'fifth');
my $result = insert_after_string 'third', 'fourth', @list;
@list; # ('first', 'second', 'third', 'fourth', 'fifth')

2つのリストを同時に操作する

@list_a = ('a', 'b', 'c');
@list_b = (1, 2, 3);
@result = pairwise { { str => $a, num => $b } } @list_a, @list_b; # ( { str => 'a', num => 1 }, { str => 'b', num => 2 }, { str => 'c', num => 3 }, )

複数のリストを1つのリストにする

@list_a = ('a', 'b', 'c');
@list_b = (1, 2);

@result = mesh @list_a, @list_b; # ('a', 1, 'b', 2, 'c', undef)
@result = zip @list_a, @list_b; # 同じ(meshのエイリアス)

zip_by で書けば、CODE BLOCKは自由に記述できるので器用なことができる

@list_a = ('a', 'b', 'c');
@list_b = (1, 2);

@result = zip_by { $_[0], $_[1] } \@list_a, \@list_b; # ('a', 1, 'b', 2, 'c', undef)

1つのリストを複数のリストに仕分ける

# CODE BLOCK は 仕分け先のインデックスを期待している
@list = (1, 2, 1, 1, 2);
@result = part { $_ } @list; # ( undef, [1, 1, 1], [2, 2] )

partition_by を使うと、仕分ける時の値をkeyとしたハッシュで返してくれる

@list = (1, 2, 1, 1, 2);
%result = partition_by { $_ } @list; # { 1 => [ 1, 1, 1 ], 2 => [ 2, 2] }

各要素を複数のリストに仕分ける

@list = ({id => 1, name => 'hoge', }, {id => 2, name => 'fuga'}, {id => 3, name => 'piyo'});
($ids, $names) = unzip_by { $_->{id}, $_->{name} } @list; # ids: (1, 2, 3), names: ('hoge', 'fuga', 'piyo')

条件ごとの要素数を求める

@list = (1, 2, 1, 1, 2);
%result = count_by { $_ } @list; # { 1 => 3, 2 => 2 }

リストからイテレータを作成する

@list = (1, 2, 3);
$it = each_array(@list);

$it->(); # 1
$it->(); # 2
$it->(); # 3
$it->(); # undef

リファレンスから作成するなら each_arrayref が使える

$list = [1, 2, 3];
$it = each_array($list);

$it->(); # 1
$it->(); # 2
$it->(); # 3
$it->(); # undef

リストから複数をまとめて返すイテレータを作成する

@list = (1...8);
$it = natatime 3, @list;

$it->(); # (1, 2, 3)
$it->(); # (4, 5, 6)
$it->(); # (7, 8)
$it->(); # undef

key-valueリスト操作

ここでのkey-valueリストとはリストの要素が (key1, value1, key2, value2, ...) となっているもの。

@kvlist = ('jp', 'こんにちは', 'en', 'hello');
%hash = @kvlist;
$hash{jp}; # こんにちは

key-valueリストをまとめて1要素にする

@list = ('k1', 'v1', 'k2', 'v2');
@result = pairs @list; # ( ['k1', 'v1'], ['k2', 'v2'] )

key, valueのまとまりを展開する

@list = ( ['k1', 'v1'], ['k2', 'v2'] );
@result = unpairs @list; # ('k1', 'v1', 'k2', 'v2')

key-valueリストのkeyのみを抽出する

@list = ('k1', 'v1', 'k2', 'v2');
@result = pairkeys @list; # ( 'k1', 'k2' )

key-valueリストのvalueのみを抽出する

@list = ('k1', 'v1', 'k2', 'v2');
@result = pairvalues @list; # ( 'v1', 'v2' )

key-valueリストから条件にあう全要素を抽出する

@list = ('k1', 'v1', 'k2', 'v2', 'k3', 'v1');
@result = pairgrep { $b eq 'v1' } @list; # ('k1', 'v1', 'k3', 'v1')

条件にあったペア数を出す

2つ1セットで見るため、最大値は要素の半分になることに注意

@list = ('k1', 'v1', 'k2', 'v2', 'k3', 'v1');
$result = pairgrep { $a eq 'k1' && $b eq 'v1' } @list; # 1

条件にあう最初の要素を抽出する

@list = ('k1', 'v1', 'k2', 'v2', 'k3', 'v1');
($key, $value) = pairfirst { $b eq 'v1' } @list; # $key = 'k1', $value = 'v1'

条件にあう要素が見つかったかを調べる

@list = ('k1', 'v1', 'k2', 'v2', 'k3', 'v1');
$result = pairfirst { $b eq 'v1' } @list; # 1

key-valueリストにmapと同じことをしたい

@list = ('k1', 'v1', 'k2', 'v2');
@result = pairmap { "$a-$b" } @list; # ( 'k1-v1', 'k2-v2' )

まとめ

PerlのモジュールであるList::AllUtilsの逆引きを作りました。 もちろんこれが正解というわけではなく、いろいろな書き方があるので、この記事を読んだ方も書き方や活用例があれば教えてください。 自分みたいに全容を理解できていない人の助けになったら嬉しいです。 最後になりますが、この記事に書くにあたって協力してくれた社員のみなさん、ありがとうございました!


明日の記事は id:summer_gift さんです!