Perl sort関数の使い方入門:配列を並べ替える方法

はい、承知いたしました。Perlのsort関数について、初心者向けに詳細な説明を含む約5000語の記事を作成します。


Perl sort関数の使い方入門:配列を柔軟に並べ替える方法

Perlは、その強力なテキスト処理能力とスクリプト言語としての柔軟性から、様々な分野で広く使われています。Perlプログラミングにおいて、データの並べ替え(ソート)は非常に頻繁に行われる操作の一つです。ファイルから読み込んだ行をアルファベット順に並べたり、数値データのリストを小さい順に並べたり、あるいはより複雑な基準でデータの集合を整理したりと、ソートの必要性は尽きません。

Perlには、このような並べ替えの要求に応えるための強力な組み込み関数であるsortが用意されています。この関数は非常に柔軟であり、単純な数値や文字列のソートから、ユーザー定義の複雑な基準に基づくソートまで、幅広いニーズに対応できます。

しかし、sort関数にはいくつかの独特な振る舞いや、効果的に使用するためのコツがあります。特に、デフォルトのソート順序、数値のソート方法、そして独自のソート基準を定義するためのブロック(またはサブルーチン)の使い方は、Perlのsortをマスターする上で避けては通れない道です。

この記事では、Perl初心者の方を対象に、sort関数の基本的な使い方から始め、数値のソート、カスタムソート、そしてより複雑なデータ構造(ハッシュや配列の参照)をソートする方法まで、段階を追って詳しく解説します。約5000語という十分なボリュームで、各概念を丁寧に説明し、多くの実用的なコード例を交えながら、Perlのsort関数を自信を持って使いこなせるようになることを目指します。

それでは、Perlのsortの世界への旅を始めましょう。

1. sort関数とは何か? なぜ必要なのか?

まず、sort関数がどのようなものか、そしてなぜプログラミングにおいてソートが重要なのかを理解することから始めます。

1.1 ソートとは?

ソート(Sort)とは、データ集合の要素を一定の基準に基づいて特定の順序に並べ替える操作のことです。例えば、数値であれば小さい順(昇順)や大きい順(降順)、文字列であればアルファベット順(辞書順)や逆アルファベット順などがあります。

1.2 なぜソートが必要なのか?

データがソートされていると、様々なメリットがあります。

  • 検索の効率化: ソート済みのデータは、特定の要素を探すのが非常に効率的になります(二分探索など)。
  • データの整理と分析: データを特定の順序で並べることで、パターンを見つけやすくなったり、最小値・最大値・中央値などを簡単に把握したりできます。
  • 表示の整形: ユーザーにデータを分かりやすく提示するために、ソートは不可欠です。例えば、商品リストを価格順に並べ替えたり、顧客リストを名前順に並べ替えたりします。
  • 重複の検出/削除: ソートされたデータでは、同じ値を持つ要素が隣り合うため、重複の検出や削除が容易になります。

Perlでは、配列やリストに格納されたデータをこれらの目的に沿って並べ替えるために、sort関数が提供されています。

1.3 Perlのsort関数の概要

Perlの組み込み関数であるsortは、リスト(または配列)を受け取り、その要素を並べ替えた新しいリストを返します。元のリストや配列は変更されません。これは非常に重要なポイントです。元の配列をソートされた状態で使いたい場合は、sortの戻り値を元の配列変数に代入し直す必要があります。

sort関数の最も基本的な構文は以下の通りです。

perl
sort LIST

または、並べ替えの基準を明示的に指定する場合の構文です。

perl
sort BLOCK LIST
sort SUBNAME LIST

ここで、LISTはソートしたい要素のリスト(配列変数でも構いません)です。BLOCKはソートの比較基準を定義するコードブロック、SUBNAMEは同じく比較基準を定義するサブルーチンの名前です。

まずは、最もシンプルな使い方から見ていきましょう。

2. 基本的な使い方:デフォルトソート

sort関数にBLOCKSUBNAMEを指定しない場合、要素はデフォルトの基準でソートされます。このデフォルト基準は「文字コード順(辞書順、ASCII順)」です。

2.1 文字列のソート

デフォルトソートは、文字列の並べ替えには直感的に理解しやすいでしょう。

“`perl
use strict;
use warnings;

my @fruits = (“banana”, “apple”, “cherry”, “date”);

my @sorted_fruits = sort @fruits;

print “元の配列: @fruits\n”; # 元の配列は変わらない
print “ソート後の配列: @sorted_fruits\n”;
“`

実行結果:

元の配列: banana apple cherry date
ソート後の配列: apple banana cherry date

これは期待通りのアルファベット順(辞書順)になっています。

2.2 数字を含む文字列のソート

デフォルトソートは文字列として要素を扱います。数字も文字として扱われるため、以下のような結果になります。

“`perl
use strict;
use warnings;

my @versions = (“v1.10”, “v1.2”, “v1.5”, “v2.0”);

my @sorted_versions = sort @versions;

print “元の配列: @versions\n”;
print “ソート後の配列: @sorted_versions\n”;
“`

実行結果:

元の配列: v1.10 v1.2 v1.5 v2.0
ソート後の配列: v1.10 v1.2 v1.5 v2.0

v1.10v1.2v1.5より前に来ています。これは、文字コード順で '1''.' より前に、'0''2''5' より前に来るため、文字列として比較すると 'v1.10' < 'v1.2' となるからです。人間が期待する「バージョン番号としてのソート」ではありません。

2.3 数値のソートにおける落とし穴

デフォルトソートが文字コード順であることの最も顕著な影響は、数値のソートで現れます。

“`perl
use strict;
use warnings;

my @numbers = (1, 10, 2, 20, 5, 15);

my @sorted_numbers = sort @numbers;

print “元の配列: @numbers\n”;
print “ソート後の配列: @sorted_numbers\n”;
“`

実行結果:

元の配列: 1 10 2 20 5 15
ソート後の配列: 1 10 15 2 20 5

この結果を見て、多くの初心者が戸惑います。なぜ2より10が前にあるのか? なぜ5が最後にくるのか?

これも理由は同じです。sortは要素を文字列として扱い、文字コード順で比較します。

  • '1''10' を比較 → '1''10' の最初の文字 '1' と同じ。次に '1' の終わりと '10''0' を比較。文字列としては '1' < '10'
  • '2''10' を比較 → '2' の文字コードは '1' の文字コードより大きい。だから '2' > '10'

つまり、デフォルトソートでは 1, 10, 15, 2, 20, 5 という順になります。これは明らかに人間が期待する数値の昇順 (1, 2, 5, 10, 15, 20) とは異なります。

重要なポイント: Perlのsortのデフォルト動作は文字コード順(辞書順)です。数値を正しくソートするには、後述する比較演算子を用いたカスタムソートが必要になります。

3. カスタムソート:BLOCKまたはSUBNAMEを使用する

数値を正しくソートしたり、デフォルトとは異なる基準でソートしたりするには、sort関数に比較基準を定義するブロック({...})またはサブルーチン名を渡す必要があります。

構文は以下のようになります。

perl
sort { ...比較ロジック... } LIST
sort サブルーチン名 LIST

このブロックまたはサブルーチンは、sort関数がリスト内の任意の2つの要素を取り出して比較する際に呼び出されます。そして、この比較ロジックの結果に基づいて、sort関数は要素の正しい順序を決定します。

3.1 比較ロジックの仕組み:$a$b

sortブロック(またはサブルーチン)が呼び出される際、Perlは比較対象の2つの要素を特別なパッケージグローバル変数 $a$b にセットします。あなたの比較ロジックは、この $a$b を使って、どちらの要素が先に来るべきかを判断し、その結果を数値で返します。

比較ロジックが返す必要のある値は以下の通りです。

  • $a$b より前に来るべき場合: 負の数 (例: -1)
  • $a$b が同じ順序と見なせる場合: 0
  • $a$b より後に来るべき場合: 正の数 (例: 1)

この「負の数」「0」「正の数」という戻り値の規約は、多くのプログラミング言語のソート関数で共通して見られるものです。

Perlでは、この比較結果を簡単に得るための専用の比較演算子が用意されています。

  • <=> (宇宙船演算子、Numeric Comparison Operator): 数値比較を行います。
    • $a < $b なら -1 を返す。
    • $a == $b なら 0 を返す。
    • $a > $b なら 1 を返す。
  • cmp (String Comparison Operator): 文字列比較を行います。
    • $a$b より文字コード順で前なら -1 を返す。
    • $a$b が同じなら 0 を返す。
    • $a$b より文字コード順で後なら 1 を返す。

これらの演算子は、ソートの比較ロジックを記述する際に非常に役立ちます。

非常に重要な注意点: $a$bsort関数によって一時的に設定されるパッケージグローバル変数です。ソートブロックの中で $a または $b の値を変更してはなりません! これを行うと、ソートの結果が不正になったり、予期しない動作を引き起こしたりする可能性があります。比較のためだけにこれらの変数の値を参照し、変更はしないようにしてください。

3.2 数値のソート(昇順・降順)

では、この比較ロジックを使って数値を正しくソートしてみましょう。

数値の昇順ソート: $a$b を数値として比較し、$a が小さければ前に来るべきなので $a <=> $b を使います。

“`perl
use strict;
use warnings;

my @numbers = (1, 10, 2, 20, 5, 15);

数値として昇順にソート

my @sorted_numbers_asc = sort { $a <=> $b } @numbers;

print “元の配列: @numbers\n”;
print “数値昇順ソート後の配列: @sorted_numbers_asc\n”;
“`

実行結果:

元の配列: 1 10 2 20 5 15
数値昇順ソート後の配列: 1 2 5 10 15 20

今度は正しく数値としてソートされました。

数値の降順ソート: 昇順とは逆に、$a が大きければ前に来るべきです。これは $b <=> $a と書くことで実現できます。

“`perl
use strict;
use warnings;

my @numbers = (1, 10, 2, 20, 5, 15);

数値として降順にソート

my @sorted_numbers_desc = sort { $b <=> $a } @numbers;

print “元の配列: @numbers\n”;
print “数値降順ソート後の配列: @sorted_numbers_desc\n”;
“`

実行結果:

元の配列: 1 10 2 20 5 15
数値降順ソート後の配列: 20 15 10 5 2 1

正しく数値の降順になりました。

3.3 文字列のソート(デフォルトとカスタム)

文字列のデフォルトソートはcmp演算子を使ったソートと同じです。

文字列の昇順ソート: デフォルトと同じですが、明示的に示すためにcmpを使います。

“`perl
use strict;
use warnings;

my @fruits = (“banana”, “apple”, “cherry”, “date”);

文字列として昇順にソート (デフォルトと同じ)

my @sorted_fruits_asc = sort { $a cmp $b } @fruits;

print “元の配列: @fruits\n”;
print “文字列昇順ソート後の配列: @sorted_fruits_asc\n”;
“`

実行結果:

元の配列: banana apple cherry date
文字列昇順ソート後の配列: apple banana cherry date

文字列の降順ソート: 数値と同様に、$b cmp $a と書くことで降順になります。

“`perl
use strict;
use warnings;

my @fruits = (“banana”, “apple”, “cherry”, “date”);

文字列として降順にソート

my @sorted_fruits_desc = sort { $b cmp $a } @fruits;

print “元の配列: @fruits\n”;
print “文字列降順ソート後の配列: @sorted_fruits_desc\n”;
“`

実行結果:

元の配列: banana apple cherry date
文字列降順ソート後の配列: date cherry banana apple

3.4 サブルーチンを使う方法

比較ロジックが複雑になる場合や、同じソート基準を複数回使いたい場合は、ブロックの代わりにサブルーチンとして定義することも可能です。

“`perl
use strict;
use warnings;

my @numbers = (1, 10, 2, 20, 5, 15);

数値昇順比較サブルーチンを定義

sub numeric_asc {
# $a, $b はサブルーチンの引数としては渡されない。
# パッケージグローバル変数としてアクセスする。
return $a <=> $b;
}

サブルーチン名を使ってソート

my @sorted_numbers_asc = sort numeric_asc @numbers;

print “数値昇順ソート後の配列 (サブルーチン使用): @sorted_numbers_asc\n”;
“`

サブルーチン内で $a$b を使う点はブロックを使う場合と同じです。サブルーチンは引数を受け取りませんが、sort関数が内部的に $a$b をセットしてからサブルーチンを呼び出します。

サブルーチンを使う方がコードが整理される場合がありますが、ブロックを使う方がシンプルでよく使われます。特に一度きりのソートの場合にはブロックが便利です。

3.5 $a$b のスコープについて

$a$b は、sort関数が評価される間だけ、現在のパッケージにおいて特別な意味を持つグローバル変数として機能します。これは、sortブロックやサブルーチンが実行される前にPerlが $a$b に値をセットし、比較ロジックの実行にこれらの変数がその比較のために使われた値を持つことを意味します。

もしあなたのコードが既に $a$b という名前のパッケージグローバル変数を使っている場合、それはsort関数によって上書きされる可能性があります。これを避けるためには、sortを使うスコープ内でlocal $a; local $b;を使うか、あるいはサブルーチン内で比較を行う場合は、そのサブルーチンを独自のパッケージに定義するというような回避策が考えられます。しかし、通常は $a$bsortのためだけに予約されたものと考え、それ以外の目的でパッケージグローバル変数として使わないのが慣習的です。

また、 lexical scope (my変数) の $a$bsort の比較変数とは関係ありません

“`perl
use strict;
use warnings;

my $a = “hello”; # これは sort の $a とは別物
my $b = “world”; # これも sort の $b とは別物

my @data = (5, 2, 8);

my @sorted_data = sort {
# ここでの $a, $b は sort の比較変数
# 外側の my $a, my $b とは異なる
print “Comparing $a and $b\n”;
$a <=> $b;
} @data;

print “Sorted data: @sorted_data\n”;
print “Outside: a=’$a’, b=’$b’\n”; # 外側の my 変数は変化しない
“`

この例からもわかるように、sortブロック内の $a, $b は外側の my 変数とは独立しています。

4. より複雑な基準でのソート

数値や単純な文字列のソートだけでなく、より複雑な基準でソートしたい場合も多々あります。例えば、文字列を大文字・小文字を区別せずにソートしたり、要素の長さに応じてソートしたりするケースです。

このような場合も、sortブロック内の比較ロジックを工夫することで対応できます。

4.1 大文字・小文字を区別しない文字列ソート

デフォルトのcmpは文字コード順なので、大文字と小文字は区別されます (‘A’ < ‘a’)。大文字・小文字を区別せずにソートするには、比較する前に両方の要素を一時的に大文字または小文字に変換してからcmpで比較します。

“`perl
use strict;
use warnings;

my @words = (“Apple”, “banana”, “Cherry”, “Date”);

大文字・小文字を区別しないソート

my @sorted_words = sort {
# lc() 関数で小文字に変換してから比較
lc($a) cmp lc($b);
} @words;

print “元の配列: @words\n”;
print “ケースインセンシティブソート後の配列: @sorted_words\n”;
“`

実行結果:

元の配列: Apple banana Cherry Date
ケースインセンシティブソート後の配列: Apple banana Cherry Date

なぜ結果が変わらないのでしょうか? これは、lc($a) cmp lc($b) の比較結果が -1, 0, 1 のいずれかであり、ソートアルゴリズムはその結果だけを見て要素の相対的な順序を決定するからです。元の 'Apple''banana' のどちらが先にくるかは、lc('Apple') cmp lc('banana') つまり 'apple' cmp 'banana' の結果にのみ依存します。その結果は -1 ですから、'Apple''banana' より前に来ます。ソート結果では、元の文字列がそのまま使われます。

では、元の配列をシャッフルして試してみましょう。

“`perl
use strict;
use warnings;
use List::Util qw(shuffle); # 配列をシャッフルするモジュール

my @words = shuffle(“Apple”, “banana”, “Cherry”, “Date”); # 毎回順序が変わる

print “シャッフル後の配列: @words\n”;

大文字・小文字を区別しないソート

my @sorted_words = sort {
lc($a) cmp lc($b);
} @words;

print “ケースインセンシティブソート後の配列: @sorted_words\n”;
“`

実行例(シャッフルにより毎回元の配列は異なりますが、結果は同じになります):

シャッフル後の配列: Date Apple banana Cherry
ケースインセンシティブソート後の配列: Apple banana Cherry Date

正しく大文字・小文字を無視してソートされました。

4.2 要素の長さでソート

文字列の長さでソートしたい場合は、length()関数を使って長さを計算し、その長さを数値として比較します。

“`perl
use strict;
use warnings;

my @words = (“apple”, “banana”, “cherry”, “date”, “fig”);

長さで昇順ソート

my @sorted_by_length = sort {
length($a) <=> length($b);
} @words;

print “元の配列: @words\n”;
print “長さでソート後の配列: @sorted_by_length\n”;
“`

実行結果:

元の配列: apple banana cherry date fig
長さでソート後の配列: fig date apple banana cherry

長さが同じ要素 (apple, date, fig) の相対的な順序はどうなるでしょうか? Perlのsort安定ソート(Stable Sort)ではありませんでした(Perl 5.8以降はデフォルトで安定ソートになりました)。安定ソートとは、比較において等しいと判断された要素の元の相対的な順序が維持されるソートのことです。Perl 5.8以降であれば、fig, date, apple の元の並び順は維持されるはずです。しかし、古いPerlバージョンや、安定ソートが保証されない状況では、同じ長さの要素の順序は保証されません。

安定ソートについては後ほど詳しく説明します。

5. 複雑なデータ構造のソート

Perlでは、配列の参照やハッシュの参照を要素として持つ配列を扱うことがよくあります。これらの複雑なデータ構造を含む配列を、その内部の値に基づいてソートする方法を学びましょう。

5.1 ハッシュのソート

ハッシュそのものを直接ソートすることはできません。ハッシュはキーと値のペアの集合であり、順序を持ちません。しかし、ハッシュのキーや値をソートして取得したり、ハッシュを格納した配列をソートしたりすることは可能です。

ハッシュのキーをソートして取得:

“`perl
use strict;
use warnings;

my %scores = (
“Alice” => 85,
“Bob” => 92,
“Charlie” => 78,
“David” => 92,
);

キーをアルファベット順にソートして取得

my @sorted_keys_by_name = sort keys %scores;

print “名前(キー)でソートした順に表示:\n”;
foreach my $name (@sorted_keys_by_name) {
print “$name: $scores{$name}\n”;
}

キーを長さでソートして取得

my @sorted_keys_by_length = sort { length($a) <=> length($b) } keys %scores;

print “\n名前(キー)の長さでソートした順に表示:\n”;
foreach my $name (@sorted_keys_by_length) {
print “$name: $scores{$name}\n”;
}
“`

実行結果:

“`
名前(キー)でソートした順に表示:
Alice: 85
Bob: 92
Charlie: 78
David: 92

名前(キー)の長さでソートした順に表示:
Bob: 92
Alice: 85
David: 92
Charlie: 78
“`

keys %scores はハッシュのキーのリストを返します。sortはそのリストをソートします。デフォルトソート(cmp)を使えばアルファベット順、カスタムソート(<=> length($a) <=> length($b))を使えば長さ順になります。

ハッシュの値をソートして取得:

“`perl
use strict;
use warnings;

my %scores = (
“Alice” => 85,
“Bob” => 92,
“Charlie” => 78,
“David” => 92,
);

値を数値の昇順にソートして取得

my @sorted_values = sort { $a <=> $b } values %scores;

print “スコア(値)をソートしたリスト:\n”;
print “@sorted_values\n”;
“`

実行結果:

スコア(値)をソートしたリスト:
78 85 92 92

values %scores はハッシュの値のリストを返します。そのリストを数値としてソートしています。ただし、この方法ではどのスコアが誰のものだったかという情報は失われます。

5.2 ハッシュの参照を要素とする配列のソート

Perlでは、複数の関連するデータのセットを扱う際、ハッシュ参照の配列(またはオブジェクトの配列)を使うことが一般的です。例えば、複数のユーザー情報を格納したハッシュ参照の配列などです。

“`perl
use strict;
use warnings;

my @users = (
{ name => “Alice”, age => 30, city => “Tokyo” },
{ name => “Bob”, age => 25, city => “Osaka” },
{ name => “Charlie”, age => 35, city => “Tokyo” },
{ name => “David”, age => 25, city => “Kyoto” },
);
“`

このような配列を、ユーザーの名前順や年齢順、あるいは都市名と年齢の組み合わせでソートしたい場合が考えられます。

sort関数に渡される $a$b は、配列の要素そのものになります。この場合、$a$bハッシュ参照です。ハッシュ参照から特定のキーの値にアクセスするには、$a->{key} または $b->{key} の形式を使います。

名前(文字列)でソート:

“`perl
use strict;
use warnings;

my @users = (
{ name => “Alice”, age => 30, city => “Tokyo” },
{ name => “Bob”, age => 25, city => “Osaka” },
{ name => “Charlie”, age => 35, city => “Tokyo” },
{ name => “David”, age => 25, city => “Kyoto” },
);

nameキーの値(文字列)で昇順ソート

my @sorted_by_name = sort { $a->{name} cmp $b->{name} } @users;

print “名前でソート:\n”;
foreach my $user (@sorted_by_name) {
print ” $user->{name}, $user->{age}, $user->{city}\n”;
}
“`

実行結果:

名前でソート:
Alice, 30, Tokyo
Bob, 25, Osaka
Charlie, 35, Tokyo
David, 25, Kyoto

$a$bがそれぞれハッシュ参照なので、$a->{name}でハッシュのnameキーの値にアクセスし、それをcmpで比較しています。

年齢(数値)でソート:

“`perl
use strict;
use warnings;

my @users = (
{ name => “Alice”, age => 30, city => “Tokyo” },
{ name => “Bob”, age => 25, city => “Osaka” },
{ name => “Charlie”, age => 35, city => “Tokyo” },
{ name => “David”, age => 25, city => “Kyoto” },
);

ageキーの値(数値)で昇順ソート

my @sorted_by_age = sort { $a->{age} <=> $b->{age} } @users;

print “\n年齢でソート:\n”;
foreach my $user (@sorted_by_age) {
print ” $user->{name}, $user->{age}, $user->{city}\n”;
}
“`

実行結果:

年齢でソート:
Bob, 25, Osaka
David, 25, Kyoto
Alice, 30, Tokyo
Charlie, 35, Tokyo

年齢が同じBobとDavid(25歳)の順序はどうなるでしょうか? Perl 5.8以降の安定ソートにより、元の配列での出現順 (Bobが先、Davidが後) が維持されています。

5.3 複数基準でのソート(Chain Comparison)

年齢が同じ場合には、名前でソートするなど、複数の基準を組み合わせてソートしたい場合があります。このような場合、比較演算子 (<=>cmp) の戻り値が 0 (等しい) だった場合に、次の比較基準に進むというロジックを記述します。Perlでは、論理OR演算子 || を使ってこれを簡潔に表現できます。

result = (comparison1) || (comparison2) || (comparison3) ... ;

これは、「comparison1 の結果が 0 でなければそれを返し、0 ならば comparison2 を評価してその結果を返す。comparison2 の結果が 0 ならば comparison3 を…」という意味になります。

例:まず年齢でソートし、年齢が同じ場合は名前でソートする。

“`perl
use strict;
use warnings;

my @users = (
{ name => “Alice”, age => 30, city => “Tokyo” },
{ name => “Bob”, age => 25, city => “Osaka” },
{ name => “Charlie”, age => 35, city => “Tokyo” },
{ name => “David”, age => 25, city => “Kyoto” },
);

まずageで昇順、ageが同じならnameで昇順ソート

my @sorted_multi = sort {
# ageを数値で比較し、結果が0(等しい)なら次の評価へ
$a->{age} <=> $b->{age} ||
# nameを文字列で比較する
$a->{name} cmp $b->{name};
} @users;

print “\n年齢、次に名前でソート:\n”;
foreach my $user (@sorted_multi) {
print ” $user->{name}, $user->{age}, $user->{city}\n”;
}
“`

実行結果:

年齢、次に名前でソート:
Bob, 25, Osaka
David, 25, Kyoto
Alice, 30, Tokyo
Charlie, 35, Tokyo

25歳のBobとDavidを見ると、年齢は同じですが、名前のアルファベット順では Bob (‘B’) が David (‘D’) より前なので、Bobが先にきています。正しく複数の基準でソートできています。

このテクニックを使えば、任意の数の基準を組み合わせてソート順序を定義できます。例えば、「都市名でソートし、同じ都市なら年齢でソートし、それも同じなら名前でソートする」といったことも可能です。

“`perl

都市名(文字列) -> 年齢(数値) -> 名前(文字列) の順でソート

my @sorted_multi_complex = sort {
$a->{city} cmp $b->{city} ||
$a->{age} <=> $b->{age} ||
$a->{name} cmp $b->{name};
} @users;

print “\n都市、次に年齢、次に名前でソート:\n”;
foreach my $user (@sorted_multi_complex) {
print ” $user->{name}, $user->{age}, $user->{city}\n”;
}
“`

実行結果:

都市、次に年齢、次に名前でソート:
David, 25, Kyoto
Bob, 25, Osaka
Alice, 30, Tokyo
Charlie, 35, Tokyo

京都のDavid (25)、大阪のBob (25)、東京のAlice (30) と Charlie (35) の順になっています。東京の二人は年齢順になっています。期待通りの結果です。

5.4 配列の参照を要素とする配列のソート

ハッシュ参照の配列と同様に、配列参照を要素とする配列もよく使われます。例えば、各要素が [名前, 年齢, 都市] のような配列参照である場合です。

“`perl
use strict;
use warnings;

my @users = (
[“Alice”, 30, “Tokyo”],
[“Bob”, 25, “Osaka”],
[“Charlie”, 35, “Tokyo”],
[“David”, 25, “Kyoto”],
);
“`

この場合、sortブロック内の $a$b配列参照になります。配列参照から要素にアクセスするには、$a->[index] または $b->[index] の形式を使います。

例:年齢(インデックス1)でソートし、同じ年齢なら名前(インデックス0)でソートする。

“`perl
use strict;
use warnings;

my @users = (
[“Alice”, 30, “Tokyo”],
[“Bob”, 25, “Osaka”],
[“Charlie”, 35, “Tokyo”],
[“David”, 25, “Kyoto”],
);

インデックス1 (年齢) で数値昇順、同じならインデックス0 (名前) で文字列昇順ソート

my @sorted_multi = sort {
$a->[1] <=> $b->[1] || # 年齢 (数値)
$a->[0] cmp $b->[0]; # 名前 (文字列)
} @users;

print “\n年齢、次に名前でソート (配列参照):\n”;
foreach my $user_ref (@sorted_multi) {
print ” @{[$user_ref->[0], $user_ref->[1], $user_ref->[2]]}\n”;
# またはよりシンプルに
# print ” @$user_ref\n”; # @$user_ref は配列参照をリストに展開する
}
“`

実行結果:

年齢、次に名前でソート (配列参照):
Bob 25 Osaka
David 25 Kyoto
Alice 30 Tokyo
Charlie 35 Tokyo

ハッシュ参照の場合と同様に、配列参照の要素にアクセスして比較ロジックを記述すれば、複雑な構造も柔軟にソートできます。

6. パフォーマンスに関する考慮事項:Schwartzian Transform

これまでの例で、sortブロック内の比較ロジックは、ソート対象の要素 $a$b を受け取り、それらを比較可能な値(数値や文字列)に変換してから比較を行っていました。例えば、length($a) <=> length($b)lc($a) cmp lc($b)、あるいは $a->{age} <=> $b->{age} などです。

ソートアルゴリズムは、要素の数 N に対して、通常 O(N log N) 回程度の比較を行います。要素が多い場合、この比較回数は膨大になります。もし、比較のために要素からソートキーを抽出する処理(length(), lc(), $a->{key} など)が比較的コストの高い操作である場合、その操作が O(N log N) 回繰り返されることになり、パフォーマンスのボトルネックになる可能性があります。

このような場合に、ソートキーの抽出を要素ごとに一度だけ行い、その抽出されたキーを使ってソートを高速化するテクニックがあります。これは「Schwartzian Transform」(シュワルツィアン変換)と呼ばれます。

Schwartzian Transformは、以下の3つのステップで構成されます。

  1. Decorate (装飾): 元のリストの各要素を処理し、「ソートキー」と「元の要素」のペア(通常は配列参照 [ソートキー, 元の要素] の形)のリストを作成します。このステップで、各要素のソートキー計算は一度だけ行われます。
  2. Sort (ソート): 作成されたペアのリストを、ペアの最初の要素(ソートキー)に基づいてソートします。このソートでは、ソートキーそのものが比較対象となるため、比較コストは低くなります。
  3. Undecorate (非装飾): ソートされたペアのリストから、元の要素だけを取り出して新しいリストを作成します。

このプロセスをコードで示すと以下のようになります。

“`perl
my @original_list = (…);

1. Decorate: [ソートキー, 元の要素] のリストを作成

my @decorated_list = map { [ compute_sort_key($), $ ] } @original_list;

2. Sort: ソートキー(ペアの0番目の要素)でソート

my @sorted_decorated = sort { $a->[0] cmp $b->[0] } @decorated_list; # cmp/ <=> はキーによる

3. Undecorate: 元の要素(ペアの1番目の要素)を取り出す

my @sorted_list = map { $_->[1] } @sorted_decorated;
“`

compute_sort_key($_) の部分は、元の要素 $_ からソートキーを計算するロジックです。この計算がコストの高い処理だと仮定します。

この3ステップは、Perlのリスト操作(map, sort, map)をパイプラインのようにつなげることで非常にPerlらしい、簡潔なコードとして記述できます。

perl
my @sorted_list =
map { $_->[1] } # 3. Undecorate (元の要素を取り出す)
sort { $a->[0] cmp $b->[0] } # 2. Sort (キーで比較)
map { [ compute_sort_key($_), $_ ] } # 1. Decorate ([キー, 元要素]ペア作成)
@original_list;

この形式は、右から左に処理が流れると考えると理解しやすいです。まず @original_listmap で装飾し、その結果を sort に渡し、その結果を再び map で非装飾しています。

6.1 Schwartzian Transform の具体例:ファイルサイズでソート

大量のファイルパスのリストを、ファイルのサイズが大きい順にソートしたいとします。ファイルのサイズを取得する stat() 関数は、単純な数値比較に比べてコストがかかる可能性があります。Schwartzian Transform を使うと、各ファイルの stat() 呼び出しを一度で済ませることができます。

“`perl
use strict;
use warnings;
use File::stat; # stat() の結果を名前付きフィールドでアクセス可能にする

my @files = glob(“*.txt”); # 現在ディレクトリの .txt ファイルリストを取得

ファイルサイズで降順にソート

my @sorted_files =
map { $->[1] } # 3. 元のファイルパスを取り出す
sort { $b->[0] <=> $a->[0] } # 2. サイズ (数値キー) で降順ソート
map { [ stat($
)->size, $_ ] } # 1. [サイズ, ファイルパス] のペアを作成
@files;

print “ファイルサイズ順 (降順):\n”;
foreach my $file (@sorted_files) {
print ” $file (“. (stat($file)->size // ‘N/A’) .” bytes)\n”; # stat()は再度呼ばれるが、表示のため
}
“`

この例では、最初の map で各ファイルパスに対して stat() を呼び出し、そのサイズとファイルパスのペアを作成しています。stat($_)->size がソートキーです。次に、そのペアのリストをサイズの降順 ($b->[0] <=> $a->[0]) でソートします。最後に、ソートされたペアから元のファイルパス ($_->[1]) だけを取り出しています。

6.2 Schwartzian Transform の具体例:ケースインセンシティブソート(効率化版)

先ほどのケースインセンシティブソートも、要素が多い場合は Schwartzian Transform を使うことで効率化できます。比較キーの計算(lc($_))が比較的回数が多い場合、有効です。

“`perl
use strict;
use warnings;

my @words = (“Apple”, “banana”, “Cherry”, “Date”, “Elderberry”);

大文字小文字を区別しない文字列ソート (Schwartzian Transform版)

my @sorted_words =
map { $->[1] } # 3. 元の単語を取り出す
sort { $a->[0] cmp $b->[0] } # 2. 小文字キー (文字列) で昇順ソート
map { [ lc($
), $_ ] } # 1. [小文字キー, 元の単語] のペアを作成
@words;

print “ケースインセンシティブソート後の配列 (Schwartzian Transform):\n”;
print “@sorted_words\n”;
“`

この例では、mapで各単語を小文字に変換したものをソートキーとしてペアを作成し、そのペアをキーでソートしています。元の比較ロジック lc($a) cmp lc($b) では、各比較のたびに $a$b の両方で lc() が呼ばれる可能性がありましたが、Schwartzian Transform では各単語に対して lc() は一度しか呼ばれません。

6.3 Schwartzian Transform のメリット・デメリット

メリット:

  • パフォーマンス向上: ソートキーの計算コストが高い場合に、計算回数を削減できるため、大幅な高速化が期待できます。
  • コードの明確化: Decorate/Sort/Undecorate の構造が明確になることで、ソートの意図が伝わりやすくなる場合があります。

デメリット:

  • 可読性の低下: 初見では何をやってるのか分かりにくいかもしれません。Perlのリスト処理に慣れていないと特にそう感じるでしょう。
  • メモリ使用量の増加: 元のリストに加えて、Decorate されたペアのリストを一時的にメモリ上に保持するため、要素が多い場合はメモリ使用量が増加します。
  • 比較が単純な場合は不要: ソートキーの計算が $a->{key} のような単純な参照の場合や、length() のような比較的軽量な関数の場合は、Schwartzian Transform によるオーバーヘッド(ペア作成、参照追跡など)の方がコスト高になる可能性もあります。パフォーマンスが問題にならない限り、シンプルな sort { ... } 形式の方が読みやすいでしょう。

Schwartzian Transform は、数千、数万といった要素を持つリストを、コストの高い基準でソートする場合に検討する価値のある強力なテクニックです。

7. その他の考慮事項

7.1 sortは常に新しいリストを返す

既に何度か触れましたが、sort関数は元のリストや配列を変更しません。必ずソートされた新しいリストを返します。

“`perl
my @original = (3, 1, 2);
my @sorted = sort { $a <=> $b } @original;

print “Original: @original\n”; # Output: Original: 3 1 2
print “Sorted: @sorted\n”; # Output: Sorted: 1 2 3
“`

もし元の配列変数 @original をソート済みの状態で使いたい場合は、戻り値を代入し直します。

“`perl
my @data = (3, 1, 2);
@data = sort { $a <=> $b } @data;

print “Data after sort: @data\n”; # Output: Data after sort: 1 2 3
“`

これは配列を「インプレース(in-place)」でソートしたように見えますが、実際には新しいソート済みリストを作成し、それを元の変数に再代入しています。

7.2 安定ソート(Stable Sort)

Perl 5.8以降のsortは安定ソートです。これは、比較ロジックにおいて等しいと判断された要素(つまり $a cmp $b$a <=> $b の結果が 0 になった要素)は、ソート前の元のリストにおける相対的な順序が維持されることを意味します。

例:年齢が同じユーザーのリストを年齢でソートする場合。

“`perl
use strict;
use warnings;

my @users = (
{ name => “Bob”, age => 25, city => “Osaka” },
{ name => “David”, age => 25, city => “Kyoto” }, # Bobより後
{ name => “Alice”, age => 30, city => “Tokyo” },
);

ageキーの値(数値)で昇順ソート

my @sorted_by_age = sort { $a->{age} <=> $b->{age} } @users;

print “\n年齢でソート:\n”;
foreach my $user (@sorted_by_age) {
print ” $user->{name}, $user->{age}, $user->{city}\n”;
}
“`

実行結果:

年齢でソート:
Bob, 25, Osaka
David, 25, Kyoto
Alice, 30, Tokyo

BobとDavidはどちらも25歳で比較結果は0になります。元のリストではBobがDavidより前だったので、ソート後もBobがDavidより前になっています。これが安定ソートの特性です。

もし安定ソートが保証されない場合、BobとDavidの順序はどちらになるか分かりません。安定ソートは、複数基準ソートで最初の基準が同じだった場合に、元の順序を維持しつつ次の基準でソートしたい場合などに暗黙的に役立ちます。

7.3 リストコンテキストとスカラコンテキスト

sort関数は、リストコンテキストで呼び出された場合はソートされたリストを返し、スカラコンテキストで呼び出された場合はソートされたリストの要素数を返します。

“`perl
use strict;
use warnings;

my @data = (3, 1, 2);

リストコンテキスト

my @sorted_list = sort { $a <=> $b } @data;
print “Sorted list: @sorted_list\n”; # Output: Sorted list: 1 2 3

スカラコンテキスト

my $count = sort { $a <=> $b } @data;
print “Number of elements: $count\n”; # Output: Number of elements: 3
“`

通常はリストコンテキストで使用することがほとんどです。

8. まとめと実践へのアドバイス

この記事では、Perlの強力なsort関数について、基本的な使い方から応用的なテクニックまでを詳細に解説しました。

  • sort LIST: デフォルトは文字コード順(辞書順)ソート。数値ソートには不向きです。
  • sort BLOCK LIST: {} ブロック内で独自の比較ロジックを定義します。
  • sort SUBNAME LIST: サブルーチンで比較ロジックを定義することもできます。
  • 比較対象の要素は特別な変数 $a$b にセットされます。これらを変更してはなりません
  • 数値比較には <=> 演算子、文字列比較には cmp 演算子を使います。比較結果は負数、0、正数である必要があります。
  • $a <=> $b で数値昇順、$b <=> $a で数値降順です。
  • $a cmp $b で文字列昇順、$b cmp $a で文字列降順です。
  • ハッシュ参照や配列参照の配列をソートする場合、$a$b はそれぞれ参照になり、$a->{key}$a->[index] のように要素にアクセスして比較します。
  • 複数基準でソートするには、|| を使って比較演算子をチェインします。
  • ソートキーの計算コストが高い場合は、Schwartzian Transform を使うことでパフォーマンスを改善できる可能性があります。これは Decorate/Sort/Undecorate の3ステップで行われます。
  • sortは常に新しいリストを返します。元のリストは変更されません。
  • Perl 5.8以降のsortは安定ソートです。

sort関数はPerlプログラミングの多くの場面で役に立ちます。この記事で学んだ知識を基に、ぜひ様々なデータを実際にソートしてみてください。最初はシンプルな数値や文字列から始め、慣れてきたらハッシュ参照の配列など、より複雑な構造のソートに挑戦してみましょう。

比較ロジックを考える上で最も重要なのは、「比較対象の2つの要素 $a$b が与えられたとき、どちらを前に置くべきか?」という問いに正しく答えられる比較結果(負、ゼロ、正)を返すコードを書くことです。数値には <=>、文字列には cmp を使うという基本を忘れなければ、ほとんどのソート要件に対応できるはずです。

Perlの強力なsort関数を使いこなし、あなたのプログラムをより効率的で分かりやすいものにしましょう!


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール