Featured image of post List::Util と List::MoreUtils - リスト処理の強力なツール

List::Util と List::MoreUtils - リスト処理の強力なツール

Perl の `List::Util` と `List::MoreUtils` を使ったリスト処理の便利な関数と使用例を紹介します。

List::Util と List::MoreUtils - リスト処理の強力なツール

Perlでリストやリストを扱う作業は日常茶飯事です。ループを書いて処理することもできますが、List::Util と List::MoreUtils を使えば、より簡潔で読みやすいコードが書けます。これらのモジュールは、関数型プログラミングのエッセンスをPerlに持ち込み、リスト処理を格段に効率化します。

List::Util - 標準のリストユーティリティ

List::Util は Perl のコアモジュール(5.7.3以降)なので、別途インストールする必要はありません。基本的なリスト操作関数を提供します。

基本的な集計関数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use List::Util qw(sum sum0 product min max);
use feature qw(say);

my @numbers = (1, 2, 3, 4, 5);

# 合計
say "合計: ", sum(@numbers);        # 15
say "合計: ", sum0(@numbers);       # 15 (空リストでも0を返す)
say "空の合計: ", sum0();           # 0

# 積
say "積: ", product(@numbers);      # 120

# 最小値・最大値
say "最小: ", min(@numbers);        # 1
say "最大: ", max(@numbers);        # 5

# 文字列での最小・最大
my @words = qw(apple banana cherry);
say "辞書順最小: ", min(@words);   # apple
say "辞書順最大: ", max(@words);   # cherry

# 数値での最小・最大(明示的)
use List::Util qw(min max);
my @mixed = qw(10 2 100 20);
say "数値最小: ", min(@mixed);     # 2 (数値として比較)

minstr / maxstr - 文字列比較

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use List::Util qw(minstr maxstr);
use feature qw(say);

my @numbers_as_strings = qw(10 2 100 20);

# 文字列として比較
say "文字列最小: ", minstr(@numbers_as_strings);  # 10
say "文字列最大: ", maxstr(@numbers_as_strings);  # 2

# 数値として比較(通常のmin/max)
say "数値最小: ", min(@numbers_as_strings);       # 2
say "数値最大: ", max(@numbers_as_strings);       # 100

reduce - 強力な集約関数

reduce は、リストの要素を順番に処理して単一の値に集約します。$a$b が特殊変数として使用されます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
use List::Util qw(reduce);
use feature qw(say);

my @numbers = (1, 2, 3, 4, 5);

# 合計(sum の実装例)
my $sum = reduce { $a + $b } @numbers;
say "合計: $sum";  # 15

# 積(product の実装例)
my $product = reduce { $a * $b } @numbers;
say "積: $product";  # 120

# 最大値(max の実装例)
my $max = reduce { $a > $b ? $a : $b } @numbers;
say "最大: $max";  # 5

# 文字列連結
my @words = qw(Hello World from Perl);
my $sentence = reduce { "$a $b" } @words;
say $sentence;  # Hello World from Perl

# 複雑な例:ハッシュの値の合計
my %scores = (
    Alice => 85,
    Bob   => 92,
    Carol => 78,
);
my $total = reduce { $a + $b } values %scores;
say "合計点: $total";  # 255

# リストのリストを平坦化
my @nested = ([1, 2], [3, 4], [5, 6]);
my @flat = reduce { [@$a, @$b] } @nested;
say "平坦化: @flat";  # 1 2 3 4 5 6

first - 条件に合う最初の要素

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use List::Util qw(first);
use feature qw(say);

my @numbers = (1, 3, 5, 8, 9, 12, 15);

# 偶数を探す
my $first_even = first { $_ % 2 == 0 } @numbers;
say "最初の偶数: $first_even";  # 8

# 10以上の数を探す
my $first_large = first { $_ >= 10 } @numbers;
say "最初の10以上: $first_large";  # 12

# 見つからない場合は undef
my $first_negative = first { $_ < 0 } @numbers;
say "最初の負数: ", $first_negative // "なし";  # なし

# ハッシュの検索
my @users = (
    { name => 'Alice', age => 25 },
    { name => 'Bob',   age => 30 },
    { name => 'Carol', age => 28 },
);

my $adult = first { $_->{age} >= 30 } @users;
say "30歳以上: $adult->{name}";  # Bob

any / all / none / notall - 真偽値チェック

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use List::Util qw(any all none notall);
use feature qw(say);

my @numbers = (2, 4, 6, 8, 10);

# いずれかが条件を満たすか
say "偶数がある: ", any { $_ % 2 == 0 } @numbers ? 'はい' : 'いいえ';  # はい
say "奇数がある: ", any { $_ % 2 == 1 } @numbers ? 'はい' : 'いいえ';  # いいえ

# すべてが条件を満たすか
say "すべて偶数: ", all { $_ % 2 == 0 } @numbers ? 'はい' : 'いいえ';  # はい
say "すべて正: ",   all { $_ > 0 } @numbers ? 'はい' : 'いいえ';        # はい

# いずれも条件を満たさないか
say "奇数なし: ", none { $_ % 2 == 1 } @numbers ? 'はい' : 'いいえ';   # はい

# いずれかが条件を満たさないか(all の否定)
my @mixed = (2, 4, 5, 8);
say "偶数でないものがある: ", notall { $_ % 2 == 0 } @mixed ? 'はい' : 'いいえ';  # はい

# 実用例:バリデーション
my @ages = (25, 30, 18, 22);
if (all { $_ >= 18 } @ages) {
    say "全員成人です";
} else {
    say "未成年が含まれています";
}

uniq / uniqnum / uniqstr - 重複削除

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use List::Util qw(uniq uniqnum uniqstr);
use feature qw(say);

# 数値として一意化
my @numbers = (1, 2, 2, 3, 1, 4, 3, 5);
my @unique_nums = uniqnum(@numbers);
say "一意な数値: @unique_nums";  # 1 2 3 4 5

# 文字列として一意化
my @strings = qw(apple banana apple cherry banana);
my @unique_strs = uniqstr(@strings);
say "一意な文字列: @unique_strs";  # apple banana cherry

# 汎用版(Perl 5.26+)
my @mixed = (1, "1", 2, "2", 1);
my @unique = uniq(@mixed);
say "一意: @unique";  # 1 1 2 2 1 (数値の1と文字列の"1"は別物)

# 実用例:重複メールアドレスの削除
my @emails = qw(
    user1@example.com
    user2@example.com
    user1@example.com
    user3@example.com
);
my @unique_emails = uniqstr(@emails);
say "一意なメールアドレス: @unique_emails";

pairs / pairkeys / pairvalues - ペア処理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use List::Util qw(pairs pairkeys pairvalues pairmap pairgrep);
use feature qw(say);

my @kvlist = (name => 'Alice', age => 25, city => 'Tokyo');

# ペアのリストに変換
my @pairs = pairs(@kvlist);
for my $pair (@pairs) {
    say "$pair->[0] => $pair->[1]";
}
# name => Alice
# age => 25
# city => Tokyo

# キーだけ取り出す
my @keys = pairkeys(@kvlist);
say "キー: @keys";  # name age city

# 値だけ取り出す
my @values = pairvalues(@kvlist);
say "値: @values";  # Alice 25 Tokyo

# ペアごとに処理(map のペア版)
my @uppercased = pairmap { uc($a) => $b } @kvlist;
say "@uppercased";  # NAME Alice AGE 25 CITY Tokyo

# ペアをフィルタ(grep のペア版)
my @filtered = pairgrep { $b =~ /\d/ } @kvlist;
say "@filtered";  # age 25

List::MoreUtils - さらに便利な関数

List::MoreUtils は List::Util を拡張し、より多くの便利な関数を提供します。

インストール

1
cpanm List::MoreUtils

zip - リストの転置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use List::MoreUtils qw(zip);
use feature qw(say);

my @names  = qw(Alice Bob Carol);
my @ages   = (25, 30, 28);
my @cities = qw(Tokyo Osaka Kyoto);

my @zipped = zip(@names, @ages, @cities);
say "@zipped";  # Alice 25 Tokyo Bob 30 Osaka Carol 28 Kyoto

# ハッシュに変換
my %people;
for (my $i = 0; $i < @names; $i++) {
    $people{$names[$i]} = {
        age  => $ages[$i],
        city => $cities[$i],
    };
}

use Data::Dumper;
print Dumper(\%people);

each_array - 複数配列の同時反復

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use List::MoreUtils qw(each_array);
use feature qw(say);

my @names = qw(Alice Bob Carol);
my @ages  = (25, 30, 28);

my $iter = each_array(@names, @ages);
while (my ($name, $age) = $iter->()) {
    say "$name is $age years old";
}
# Alice is 25 years old
# Bob is 30 years old
# Carol is 28 years old

part - 条件によるグループ分け

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use List::MoreUtils qw(part);
use feature qw(say);

my @numbers = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

# 偶数と奇数に分類
my $i = 0;
my @groups = part { $_ % 2 } @numbers;

say "偶数: @{$groups[0]}";  # 2 4 6 8 10
say "奇数: @{$groups[1]}";  # 1 3 5 7 9

# 3つのグループに分類(3で割った余りで)
my @three_groups = part { $_ % 3 } @numbers;
say "3で割り切れる: @{$three_groups[0] // []}";  # 3 6 9
say "余り1: @{$three_groups[1] // []}";          # 1 4 7 10
say "余り2: @{$three_groups[2] // []}";          # 2 5 8

before / after - 条件の前後で分割

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use List::MoreUtils qw(before after);
use feature qw(say);

my @numbers = (1, 2, 3, 4, 5, 6, 7, 8);

# 5より小さい要素
my @before_five = before { $_ >= 5 } @numbers;
say "5未満: @before_five";  # 1 2 3 4

# 5以上の要素
my @after_five = after { $_ >= 5 } @numbers;
say "5以上: @after_five";  # 5 6 7 8

indexes - 条件を満たすインデックス

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use List::MoreUtils qw(indexes);
use feature qw(say);

my @numbers = (10, 25, 30, 15, 40, 20);

# 20以上の要素のインデックス
my @indices = indexes { $_ >= 20 } @numbers;
say "20以上のインデックス: @indices";  # 1 2 4 5

# 実用例:該当要素を削除
my @words = qw(apple banana cherry date elderberry);
my @to_remove_indices = indexes { length($_) > 6 } @words;

# 後ろから削除(インデックスがずれないように)
for my $idx (reverse @to_remove_indices) {
    splice @words, $idx, 1;
}
say "残った単語: @words";  # apple banana cherry date

firstidx / lastidx - インデックスを返す first

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use List::MoreUtils qw(firstidx lastidx);
use feature qw(say);

my @numbers = (1, 3, 5, 7, 5, 3, 1);

# 最初の5のインデックス
my $first_five = firstidx { $_ == 5 } @numbers;
say "最初の5: インデックス $first_five";  # 2

# 最後の5のインデックス
my $last_five = lastidx { $_ == 5 } @numbers;
say "最後の5: インデックス $last_five";  # 4

# 見つからない場合は -1
my $not_found = firstidx { $_ == 10 } @numbers;
say "10のインデックス: $not_found";  # -1

minmax - 一度に最小値と最大値を取得

1
2
3
4
5
6
7
8
9
use List::MoreUtils qw(minmax);
use feature qw(say);

my @numbers = (3, 1, 4, 1, 5, 9, 2, 6);

my ($min, $max) = minmax(@numbers);
say "最小: $min, 最大: $max";  # 最小: 1, 最大: 9

# 1回のループで取得できるため効率的

実用的なリスト処理パターン

データの集計と分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use List::Util qw(sum max min);
use List::MoreUtils qw(minmax);
use feature qw(say);

my @scores = (85, 92, 78, 90, 88, 95, 73);

my $total = sum(@scores);
my $average = $total / @scores;
my ($min, $max) = minmax(@scores);

say "=== スコア統計 ===";
say "合計: $total";
say "平均: ", sprintf("%.2f", $average);
say "最高点: $max";
say "最低点: $min";
say "範囲: ", $max - $min;

# 中央値を求める
my @sorted = sort { $a <=> $b } @scores;
my $median = @sorted % 2 
    ? $sorted[@sorted / 2]
    : ($sorted[@sorted / 2 - 1] + $sorted[@sorted / 2]) / 2;
say "中央値: $median";

ログファイルの解析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use List::Util qw(sum max);
use List::MoreUtils qw(part);
use feature qw(say);

my @log_lines = (
    'INFO: Process started',
    'WARN: Low memory',
    'ERROR: Connection failed',
    'INFO: Process completed',
    'ERROR: Timeout',
    'WARN: High CPU usage',
);

# レベル別に分類
my @by_level = part {
    /^INFO/  ? 0 :
    /^WARN/  ? 1 :
    /^ERROR/ ? 2 : 3
} @log_lines;

say "INFO: ",  scalar(@{$by_level[0] // []}), " 件";
say "WARN: ",  scalar(@{$by_level[1] // []}), " 件";
say "ERROR: ", scalar(@{$by_level[2] // []}), " 件";

# ERROR のみ表示
if ($by_level[2]) {
    say "\n=== エラーログ ===";
    say for @{$by_level[2]};
}

ショッピングカートの処理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use List::Util qw(sum reduce);
use feature qw(say);

my @cart = (
    { name => 'リンゴ',   price => 100, quantity => 3 },
    { name => 'バナナ',   price => 150, quantity => 2 },
    { name => 'みかん',   price => 80,  quantity => 5 },
);

# 各商品の小計
my @subtotals = map { $_->{price} * $_->{quantity} } @cart;

# 合計金額
my $total = sum(@subtotals);

say "=== お買い物カート ===";
for my $item (@cart) {
    my $subtotal = $item->{price} * $item->{quantity};
    say sprintf("%s: %d円 × %d = %d円",
        $item->{name}, $item->{price}, $item->{quantity}, $subtotal);
}
say "合計: ${total}円";

# 割引適用(1000円以上で10%オフ)
if ($total >= 1000) {
    my $discounted = int($total * 0.9);
    say "割引後: ${discounted}円(10%オフ)";
}

ユーザーデータのフィルタリングとソート

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use List::Util qw(all any);
use List::MoreUtils qw(uniq);
use feature qw(say);

my @users = (
    { name => 'Alice', age => 25, active => 1 },
    { name => 'Bob',   age => 30, active => 1 },
    { name => 'Carol', age => 28, active => 0 },
    { name => 'Dave',  age => 22, active => 1 },
    { name => 'Eve',   age => 35, active => 0 },
);

# アクティブユーザーのみ
my @active_users = grep { $_->{active} } @users;
say "アクティブユーザー: ", scalar(@active_users), "人";

# 25歳以上のアクティブユーザー
my @adult_active = grep { $_->{age} >= 25 && $_->{active} } @users;
say "25歳以上のアクティブユーザー: ";
say "  - $_->{name} ($_->{age}歳)" for @adult_active;

# 年齢でソート
my @sorted_by_age = sort { $a->{age} <=> $b->{age} } @users;
say "\n年齢順:";
say "  $_->{name}: $_->{age}歳" for @sorted_by_age;

# 全員25歳以上か?
say "\n全員25歳以上: ", all { $_->{age} >= 25 } @users ? 'はい' : 'いいえ';

# 誰か30歳以上がいるか?
say "30歳以上あり: ", any { $_->{age} >= 30 } @users ? 'はい' : 'いいえ';

パフォーマンス考慮

ループ vs List::Util

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use Benchmark qw(cmpthese);
use List::Util qw(sum);

my @numbers = (1..10000);

cmpthese(10000, {
    'loop' => sub {
        my $sum = 0;
        $sum += $_ for @numbers;
    },
    'List::Util::sum' => sub {
        my $sum = sum(@numbers);
    },
});

多くの場合、List::Util の関数は C 実装されているため、Perlのループより高速です。ただし、非常に単純な処理では、オーバーヘッドによりループの方が速い場合もあります。

適切な関数の選択

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use List::Util qw(first any);
use feature qw(say);

my @numbers = (1..1000000);

# 存在チェックだけなら any を使う(効率的)
say "偶数あり: ", any { $_ % 2 == 0 } @numbers ? 'はい' : 'いいえ';

# 値が必要なら first を使う
my $first_even = first { $_ % 2 == 0 } @numbers;
say "最初の偶数: $first_even";

# 全部必要なら grep を使う
my @evens = grep { $_ % 2 == 0 } @numbers;
say "偶数の個数: ", scalar(@evens);

まとめ

List::Util と List::MoreUtils は、Perlでのリスト処理を劇的に改善します。

よく使う関数

  • 集計: sum, product, min, max
  • 検索: first, any, all, none
  • 変換: reduce, uniq
  • ペア処理: pairs, pairmap, pairgrep

List::MoreUtils の便利機能

  • 分類: part
  • インデックス: indexes, firstidx, lastidx
  • 複数配列: zip, each_array

これらの関数を使うことで:

  • コードが短く読みやすくなる
  • バグが減る(ループの範囲エラーなど)
  • 意図が明確になる
  • パフォーマンスが向上することが多い

リスト処理は Perl の得意分野です。これらのツールを活用して、効率的で美しいコードを書きましょう!

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。