Skip to content

Instantly share code, notes, and snippets.

@nkoneko
Last active March 14, 2024 23:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nkoneko/3a9d7ce4100eda39005a94ab35a12f41 to your computer and use it in GitHub Desktop.
Save nkoneko/3a9d7ce4100eda39005a94ab35a12f41 to your computer and use it in GitHub Desktop.
Perlは今でも進化を続けている言語だが、とはいえ現実にレガシーなコード以外ではなかなかお目にかかれない。昔書かれたPerlのコードを読み解き、メンテするのは気が重い。そんなレガシー...古代言語のPerlを読むための確かな知識を獲得するための入門文書がこれだ

古代言語Perl入門

文法を完全に理解しよう

文字列

二重引用符(ダブルクォート " )と引用符(クォート ' )はどちらも文字列のリテラルを表すのに利用できます。これらの違いを説明します。

まずひとつめの違いとして、二重引用付では変数を含めて、変数を文字列の中で展開できます。例えば、次のコードを実行すれば、 the answer is 42 と出力するでしょう。

$answer = 42;
print("the answer is $answer");

この例のような文字列リテラルでの変数の展開、埋め込みのことを string interpolation というそうです。

これに対して、次の例のように二重ではない引用符を使った場合は変数は展開されずに the answer is $answer と出力されます。この挙動は似た機能を持つ多くのプログラミング言語で共通ですし、bashのようなシェルでも同じですので覚えやすいですね。

$answer = 42;
print('the answer is $answer');

また、二重引用符を利用する場合はエスケープシーケンスを利用できます。そのため、改行やタブを入れたい場合は二重引用符を使用することになるでしょう。

セミコロン

Perlではセミコロン ; によって文を区切ります。 例えば

$answer = 42;
print($answer);

は構文的に問題ありませんが、以下のようにセミコロンを忘れてしまうと、Perlの構文解析器は文の切れ目を把握できず、文が終わる前に予想外のトークンに遭遇したとして構文エラー(Syntax Error)をエラー出力して実行せずに終了します

$answer = 42
print($answer)

# syntax error at - line 2, near "print"
# Execution of - aborted due to compilation errors.

C言語やJava言語と同様に論理的な行が終わるごとにセミコロンをつける習慣をつけると良いでしょう。

なお、Perlでセミコロンを省略しても構文エラーにならずに正しく動くケースもあります。これはブロック末尾やファイル末尾の行でセミコロンがなくとも構文の解釈に曖昧さがないケースです。 例えば、次のPerlスクリプトは構文エラーをエラー出力しません。

@tokens = ();
while (<>) {
  chomp;
  push @tokens, split
}
foreach $token (@tokens) {
  print($token, "\n")
}

print("finished.\n")

関数と演算子の優先度

Perlでは組み込み関数として利用可能なものとして、named unary operatorと呼ばれるものとlist operatorと呼ばれるものがあります。 named unary operatorは一つの引数を取り、list operatorは1つ以上の複数の引数を取ります。 ここで、重要な構文上の規則を説明しておきます。 これらの関数は、例えば算術演算子の + に括弧が必要ない、すなわち ((1) + (2)) のように書く必要がないこととよく似ていて、構文上の制約でそう書かざるを得ない場合を除いては 括弧は必要ありません

例えば、chmopというnamed unary operatorは文字列を引数として改行文字を削除した文字列を返しますが

$line = chomp($line);

ではなく

$line = chomp $line;

と書いて差し支えありません。

list operatorも同様で、例えばprintはlist operatorですので

print("Hello", "\n", "World\n");

と書こうが

print "Hello", "\n", "World\n";

と書こうがどちらでも構わないことになります。

他の例として、ファイルを開くときに使うopenもlist operatorです。ですので

open FH "example.pl";

(open FH, "example.pl");

と解釈されます。また、list operatorの直後に現れたトークンが ( であるとき、その括弧を閉じるまでが一つの項(TERM; (1+2)*3 の (1+2) の部分みたいなやつのことです) として解釈され、そしてこの場合はlist_operatorの後ろのTERMが引数として解釈されます。なので、例えば上のコードは

open(FH, "example.pl");

と書いても同じように解釈されます。こちらの方が多くのプログラミング言語の経験者に馴染みのある書き方ですので、この入門では原則としてこの書き方をします。また、

print "a", "b", "c";

は abc を出力します。 (print "a", "b", "c") と書いたのと同じと解釈されてこの3つの文字列が全て引数になるわけです。

とはいえ、プログラミングの経験自体はあるがPerlを使い込んでいるというわけでない多くの人にはこれでは意図が伝わりにくいので、個人的には括弧を明示的につけるスタイルをお勧めします。また、括弧で引数を括るようにした方が意図しないバグの原因も減らせます。これはどういうことかと言うと、構文解析器の都合と言いますか、operator precedence(演算子の結合の優先度)が必ずしも直感的ではないからです。

operator precedenceとはどういうことかというと、わかりやすく算術演算子の +* を例に挙げますと

1 + 2 * 3

(1 + 2) * 3

ではなく、

1 + (2 * 3)

と解釈されますよね。これは * の方がより高い演算子の優先度 (operator precedence) を持っているからです。list operatorのoperator precedenceは括弧で括った式と同じレベルの優先度ということになっています。list operatorは直後に ( が出現しない場合は、後続するリストを引数にとるわけですが、リストはコンマ演算子(comma operator)によって構築されます。

Perlではリストを要求するコンテクストではコンマ演算子によって、 expr1, expr2 は、 expr1 がリストでない場合はメモリを割り当てて空のリストを作成し、 expr1 の評価値をリストに追加してから expr2 の評価値も追加してリストを返すという挙動になっています。このコンマ演算子のoperator precedenceが曲者で、実は || 演算子よりも優先度が低いのです。

これはどういうことかというと、

a, b, c || d, e

(a, b, c) || (d, e)

ではなく

(a, b, (c || d), e)

と解釈されるということです。わかりにくいですかね?先ほどのように +* で考えると

a + b + c * d + e

と書いているのと同じルールが適用されるので

a + b + (c * d) + e

と同じように解釈されますよね。

ですので

open FH, "example.pl" || die "failed to open.

は期待通りに動きません。これは

(open FH, "example.pl" || (die "failed to open."));

と解釈されますが、 || の左側のオペランドが空でない文字列で true として解釈されますので、実は右側のオペランドは絶対に評価されることのないデッドコードになっており

(open FH, "example.pl");

というファイルのオープンに失敗したときのエラーハンドリングが存在しないコードと同じ振る舞いになります。これは明示的に括弧をつけることで防げます

open(FH, "example.pl") || die "failed to open.";

ならば

(open FH, "example.pl") || (die "failed to open.");

と同じ解釈になるわけですね。こうした意図しないバグを生み出さないためにも、冗長ではあるのですが括弧をつけることをお勧めします。

なお、 ,or よりも優先されるので

open FH, "example.pl" or die "failed to open";

と書いた場合は

(open FH, "example.pl") or (die "failed to open");

として解釈されます。ただ、いちいちoperatorのprecedenceを覚えていて意識的に読み書きする人は少ないですので、先ほどの括弧を使った記法が間違いが少ないと思います。 

変数

変数はプログラミング言語において非常に重要な概念ですが、Perlはややとっつきにくいところがありますので、具体例を確認しながら解説します。ここでは参照については説明しません。しかし、Perlでのプログラミングでは参照の理解が欠かせないので、必ず後述する参照の説明も読むようにしてください。

さて、プログラミング言語が仕事をするためには、変数名とメモリ上に存在するオブジェクトの対応付けを管理することが必要です。対応付けることを束縛という言い方をしますが、それはここではどうでもいいです。対応付けがあるからこそ名前によって参照できるわけですね。

Perlでは名前とオブジェクトの関連付けの管理において、スカラー、配列・ハッシュ、コードを区別しています。スカラーというのはPerlでは数値、文字列または参照を表します。配列、ハッシュについては後ほど説明します。 このような区別があるため、例えば数値と紐付いている変数 $answer とハッシュの %answer は名前こそどちらもanswerですが実行環境で厳密に区別されます。なので、以下のコードを実行すると、二つの57ではなく、42と57が出力されます。

$answer = 42;
%answer = (
  value => 57
); # この代入文の左辺は、ハッシュのanswerであり、スカラーのanswerではないので、前に宣言・初期化したanswerを上書きしない
print($answer, "\t", $answer{value});

変数名の先頭の記号はsigilと呼ばれていますが、スカラーの場合は $ 、配列は @ 、ハッシュは % 、コードは & (だがあってもなくとも良い) という感じに型によって使い分けています。

スカラー

文字列と数値はスカラーですので、これらの値に紐付ける変数名の先頭(sigial)には $ をつけます

$hello = 'Hello, World!';
print($hello); # Hello World!
 
$answer = 40 + 2;
print($answer); # 42

次に配列について学びますが、配列はsigilが異なるものの、配列の要素(スカラーになります)を参照するときは以下のようにスカラーを表すsigil ($) を使います。

$array[0]

配列とリスト

配列を表すsigilは @ です。Perlでは配列とリストはほぼ同じ意味で使われますが、厳密には異なるものです。変数名のついている変数は配列と呼びますが、リテラルで書かれるようなスカラーの列はリストと呼んでいて多くは代入で現れます。多くの場合は特に区別して理解しなくとも良いのですが、後述しますが例えば配列のスライスとリストのスライスで振る舞いが異なるということもあり、違いを理解しておくことは大切です。

リストを生成するには、コンマを使います。スカラーをコンマで区切ればリストと解釈されます。

1, 2, 3 # これはリストです

#なので...
@array = (1, 2, 3); # @array は変数であり配列です。右辺は1, 2, 3というリストです
push @array, 2, 3, 4 # この 1, 2, 3もリストです

この例のように、代入でリストを扱うには、一般に括弧(())を使います。これは構文上、括弧で括らないと異なった構文解釈をされてしまうからです。この点はまた後ほど詳しく説明します。

配列には数値のリストに限らず、文字列も、その混在も、スカラーのリストであれば何でも代入できます。 

@architecutres = ('x86', 'ia64', 'arm64');

多くの言語と同様に [ ] によってindexでの要素へのアクセスが可能です。Perlは多くのプログラミング言語と同様にindexは0から始まります。

以下のコードでは、配列 @architectures の1番目の要素自体はスカラーなので、代入文の左辺も右辺もどちらもsigilが $ になっていることに注意してください。Perlでは配列を意図しているのにsigilを誤って $ で書いてしまったり、あるいは配列の1つの要素を取り出しているからスカラーなのに誤って @architectures[0] のように書いてしまい想定通りに動かずに悩んで時間を溶かすというミスをしがちなので注意しましょう。

@architectures = ('x86', 'ia64', 'arm64');
$x86 = $architectures[0];
print("$x86\n"); # x86

多くのプログラミング言語と同様に、配列の要素を順に取り出すための構文として、foreachを利用できます。 foreachで取り出した個々の要素を紐づけておく変数は配列の前に以下のコードの $arch のように配置します。 配列を配列として参照するときはsigilが @ で、スカラーの値である個々の要素と紐づく変数のsigilは $ になっていることに注意してください。

foreach $arch (@architectures) {
  print("$arch\n");
}

ちなみに、Perlに慣れた人以外には読めなくなるので個人的にはあまり推奨しませんが、以下のような省略が可能です。

foreach (@architectures) {
  print;
  print("\n");
}

Perlのいくつかの関数は引数の指定がないときデフォルトで $_ という特殊な変数を引数として取ることになっており、このforeachの作る文脈では配列から取り出してきた要素がループごとに $_ に代入されているから、このような書き方ができます。 ただし、Perlに慣れている人以外では読みにくくなってしまうので、あまり多用しないのが無難かと思います。

また、foreachで配列やリストの要素を取り出すとき、各要素の値を取り出しているというよりは、各要素の変数のエイリアスを作っており、foreach文の中で配列の中身を書き換えることができます。

@maga = ('make', 'america', 'great', 'again');
foreach $word (@maga) {
  $word = uc($word); # ucはuppercase
}
print(join(' ', @maga)); # MAKE AMERICA GREAT AGAIN

なお、省略して書く場合はこうです(お勧めしません)

@maga = ('make', 'america', 'great', 'again');
foreach (@maga) {
  $_ = uc;
}
print(join(' ', @maga));

配列ではなくリストも [] を使った構文や foreach 文で要素を取り出すことができます。

print(('hello', 'goodbye')[0], "\n"); # hello
foreach $word ('hello', 'goodbye') {
  print("$word\n");
}
# hello
# goodbye
# と出力

foreach文の中ではリストには括弧を付けていないことに注意しましょう。代入のような構文上の混乱のないところでは、リストを作る際にはコンマでスカラーを区切れば十分で実は括弧は不要だということは一貫した単純な規則でPerlプログラムを理解することの役に立ちますので、是非覚えておいてください。

また、foreach文の () の中のようにリストを要求するコンテクストというものがあり、リストを要求するコンテクストに配列を配置すると、その配列を配列の要素を並べたリストとして展開するという解釈がされます。すなわち、先ほどの配列をforeach文で取り出す例は

foreach $arch ($architectures[0], $architectures[1], $architecturs[2]) {
  print("$arch\n");
}

のように要素のリストに展開しているのと意味としては同じ扱いになります。この点も一貫した単純な規則でPerlプログラムを理解する助けになりますので是非頭の片隅に置いてください。

また、リストを要求する文脈でコンマ , を使ってリストを作るとき、式を評価した結果がスカラーのリストとなるよう、コンマの左右にリストや配列が現れても、全てスカラーとしてその場に展開して一つの同じリストに挿入するような挙動となります。他の多くのプログラミング言語でいうところの flatten のような挙動です。

@numbers1 = (5, 6, 6);
@numbers2 = (8, 9, 10);

# 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 というリストになる
@numbers3 = ((1, 2, 3), 4, @numbers1, 7, @numbers2);

# ()の中は 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 というリストになる
foreach $number (0, @numbers) {
  print("$number\n");
}
# 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9
# 10
# と出力する

foreach文以外にもC言語のようなスタイルのfor文で要素を取り出すことも可能です。

for ($i = 0; $i < 3; $i++) {
  print($architectures[$i]);
}

ループカウンター用の変数として $i を 0 で初期化して、3未満であるという条件を満たす限りはループを繰り返す。ループのたびに $i++ を実行してインクリメントする、というC言語などと同じ意味で捉えて大丈夫です。(ちなみに、実はforをforeachに書き換えても動きます。forとforeachはシノニムではあります)

さて、多くの言語では配列の長さを取得するための配列自体の属性や関数があります。JavaやJavaScriptでは array.length , Pythonでは組み込み関数の len() などがありますね。Perlにもそういった関数があると便利そうです。

実はPerlには配列の長さを取得する関数はありません。ただ、取得する方法自体はありますのでご安心ください。

先ほどforeachの ()リストを要求するコンテクスト という言い方をしたのを覚えていますか? Perlでは式を評価した値はこのコンテクストという概念によって説明できます。コンテクストによって同じ式でも異なる値になることがあるということでもあります。 リストを要求するコンテクスト以外に、スカラーを要求するコンテクストというものがあります。配列の値はリストを要求するコンテクストでは先ほど申し上げた通り、配列の要素を列挙したリストとして解釈されますが、スカラーを要求するコンテクストでは配列に含まれる要素の個数になります。

では、スカラーを要求するコンテクストはどのようにすれば作れるのでしょうか? 数値の比較演算子 <, =, >, >=, <= や左辺をスカラー変数とする代入の右辺がスカラーを要求するコンテクストを作ります。 ですので、以下の各行の式で、 @architectures はスカラーのコンテクストにより 3 という値として解釈されます。

@architectures = ('x86', 'ia64', 'arm64');

my $count = @architectures; # 左辺がスカラーの代入なので、右辺はスカラーを要求するコンテクスト。よって $count == 3

for ($i = 0; $i < @architectures; $i++) { # < の左右はスカラーを要求するので、 $i < 3 と解釈される
  print($architectures[$i], "\n");
}

ちなみに、スカラーを要求するコンテクストで配列ではなくリストを書くと、リストの長さではなくリストの最後の要素が返ってくるような挙動になります。 

$number = (9, 5, 7);
print("$number\n");  # 7

これは厳密には、実はスカラーを要求するコンテクストにはリストは書けないというのが正しかったりします。Perlの構文としては、コンマ演算子(comma operator)というのもがあり、コンマ演算子は出現する位置によって異なる意味で解釈されます。リストを要求するコンテクストではコンマ演算子はリストの要素を区切る文字として解釈されます。しかし、スカラーを要求するコンテクストでは、コンマの左の式を評価してから右の式を評価し、右の式の値を返すという振る舞いになります。

スカラーコンテクストを強制するscalar演算子(scalar operator)もあり、暗黙に演算子が要求するコンテクストを使うよりも、これを使ったほうがとコードを理解しやすくなるかもしれません。(好みによるところも大きいかもしれません)

for ($i = 0; $i < scalar(@architectures); @i++) {
  print("$architectures[$i]\n");
}

配列の長さの取得としてもう一つ重要な方法を紹介します。厳密には配列の長さというよりは、配列の最後の要素のindexを取得する方法になります。 変数名の先頭のsigilによって変数の意味が異なることはこれまでにも説明していますが、 $# というsigilによって配列の最後の要素のindexを取得することができます。 $#architectures はこの場合は 2 になります

for ($i = 0; $i <= $#architecture; $i++) {
  print($architectures[$i]);
}

配列・リストに関しては、まだ理解しておくべき構文があります。長くなりますが、いましばらくお付き合いください。 次に代入文の左辺でのリストについて説明します。

代入文の左辺にリストを書くことができ、これによって、左辺のリストの中の左辺値に右辺のリストの値を順次代入することができます。この説明だと分かりにくいと思いますので具体例をあげます。

($x, $y, $z) = (1, 2, 3);
print('$x + $y + $z = ' . ($x + $y + $z) . "\n"); # $x + $y + $z = 6
# . は文字列を連結する演算子で、($x + $y + $z)は数値だが文字列として変換されてから連結される

上のコードでは、代入の左辺に ($x, $y, $z) という3つのスカラーの変数のリストを置いており、右辺のリストのそれぞれ対応付く値が変数に代入されています。 この代入は左辺のリストに配列が含まれる場合には、配列への右辺の対応する位置のスカラーのリストを代入するものと解釈されます。 これもコードなしだと分かりにくいと思いますので、具体例を示します。

($x, @a) = (1, 2, 3, 4); # $x = 1, @a = (2, 3, 4) と同じ
foreach $a (@a) {
  print("$a\n");
}
# 2
# 3
# 4
# と出力される

ここで注意すべきことは、Perlでは配列の要素数の上限を指定することはできませんので、左辺をリストにするこの形式での代入は、左辺のリストに配列が現れると、対応する右辺のリストの位置のスカラーから末尾までの全ての要素が代入され、左辺のリストにそれ以上変数を続けてもその変数への代入は発生しないということです。これも分かりにくいのでコードを書きますね。

@a = (0, 0, 0);
($x, @a, $y) = (1, 2, 3, 4, 5);
foreach $n (@a) {
  print("$n\n");
}
# 2
# 3
# 4
# 5
# と出力

最初の代入文があると、なんとなく、@aの長さが3に固定されそうな気がしてしまいます。 それにより、 $x = 1, @a = (2, 3, 4), $y = 5 になるといいなあ...と思ってしまいがちですが、残念ながらそうはなりません。 @a の長さの上限は与えられないので、リストの末尾まで全ての要素を食うような挙動になります。すなわち、 $x = 1, @a = (2, 3, 4, 5) となり、 $y には何も代入されず未定義、すなわち $y = undef になります。

この挙動の理解はのちにサブルーチンを学ぶ際に期待通りのコードを書くために必要な知識なのでおさえておいて

最後にリストのもうひとつの重要な記法があるので、そちらも紹介しますね

qw(this is a pen)

これは

'this', 'is', 'a', 'pen'

というリストと同じです。つまりは、空白区切りで引用符をつけずにリストを書いているのと同じというわけです。なお、 qw の次の文字は ( である必要はありません。割とどの記号でも大丈夫です。対応付けされる文字で最後に閉じられていれば...

いくつか具体例を書いておきます

qw(this is a pen);
qw/this is a pen/;
qw[this is a pen];
qw*this is a pen*;
qw-this is a pen-;
qw\this is a pen\;

とはいえ、慣習としては ()// を使うことがほとんどですので、それに従っておけば良いでしょう

わざわざ色々な記法を混ぜるのも良くないのですが、例えばこれまでの知識から、以下のコードは ('to', 'be', 'or', 'not', 'to', 'be') というリストだということも理解できます。

@br_or = ('be', 'or');
'to', @be_or, qw/not to/, qw(be)

なお、 q 自体が特別な意味を持っており、これは次の文字を引用符の代わりに使うという感じの意味になります。(括弧の場合は対応付く文字で閉じる必要があります。例えば 'cats and dogs'q(cats and dogs)q=cats and dogs= とも一応はかけます 

ハッシュ

他の言語で連想配列だとかハッシュ表、辞書などと呼ばれているものは、Perlでは単にハッシュといいます。sigilは % です。 ハッシュリテラルは、括弧で括った key => value のコンマ区切りの集まりとして記述できます。

%abbreviation = (
  'ACM' => 'Association for Computing Machinery',
  'IEEE' => 'Institute of Electrical and Electronics Engineers'
);
$ieee = %abbreviation{'IEEE'};

これは実はちょっと嘘を含んでおり、まず実はリストと同じで代入など一部の構文上の制約があるケースを除いて、ハッシュもまた括弧(())は必要ありません。また、実は =>, とほぼ同じ意味で、あくまで読みやすさのために導入されたシノニムです。

実際、次のようなコードは 'a', 'b', 'c', 'd' という配列を変数 @a に代入します。

@a = (
  'a' => 'b',
  'c' => 'd'
);

あるいは次のコードでも同じ結果が得られます。

@a = ( 'a' => 'b' => 'c' => 'd' );

そうです。実はハッシュリテラルなんてものは存在しません。(後述しますが、ハッシュを作ってその参照を得るリテラルは存在します) 左辺がハッシュの代入の右辺はリストを要求するこんてコンテクストを作ります。ですので、(括弧で括った)コンマ演算子はリストを作ります。そして、そうして作られたリストをハッシュの変数に代入すると、ハッシュとして利用可能となるというだけです。

補足として、わずかに異なるところもあるので、 ,=> の違いも述べておきます。 まず、前提として実は(use strictを指定していなければ)、引用符のない文字列も引用符のある文字列と基本的には同様に扱われます。barewordと言います。 コードを書いて試してみましょう

no strict 'subs';
@hello = ( you => 'say goodbye', I => 'say hello');

このコードを実行すると 'you', 'say goodbye', 'I', 'say hello' という文字列のリストが得られます。 strictによる制約を無効にしていると、以下のコードでも同じリストが得られます。

no strict 'subs';
@hello = ( you, 'say goodbye', I, 'say hello');

しかし、同名のサブルーチンがある場合やそのサブルーチンの呼び出しと解釈されますし、予約語も入れることはできません。しかし、 => の左側でbarewordを使った場合はサブルーチンや予約語として解釈されず常に引用符のある文字列と同じように扱われます。これは実際にそのようなコードを書くことで確かめられます。

no strict 'subs';

sub hello {
  'Hello, world!'
}

@hello1 = ( hello, world );
@hello2 = ( hello => world );

foreach (@hello1) {
  print;
  print "\n"
}
# Hello, world!
# hello
# と出力

foreach (@hello1) {
  print;
  print "\n"
}
# hello
# world
# と出力

実は構文上の制約がなければ括弧は不要ということと、 => は実は , とほぼ同じ扱いということを理解していると、単純で一貫した規則で理解しやすいコードが多いですので、この点も頭の片隅においていただけると良いかと思います。

keys関数を使うことによって、ハッシュからキーのリストを取り出すことができます。これによって、foreach文で全要素を辿ることも簡単にできます。

foreach $key (keys %abbreviation) {
  print("key: $key\n");
  print("value: $abbreviation{$key}\n");
}

キーに対応付いた要素を取り出すとき、要素はスカラーですので、sigilが $ になっていることに注意しましょう。(Perlはここが気持ちの悪いところでもあり、ミスしやすいポイントですが、慣れましょう)

ハッシュの個々のキーに関連付いた値を取り出すには、ブレース ({}) を使います。この中にキーを書くことで要素を取り出すことができます。個々の要素はスカラーですので、sigilが $ になることも気をつけましょう。

%translation = (
  'night' => 'Nacht',
  'morning' => 'Morgen'
);
print($translation{'nigth'} . "\n"); # Nacht

ハッシュのキーに対応付く値を取り出すこの構文では、ブレースの中はbarewordが使えます。そのため、先ほどの => の左側は常に引用符付きの文字列として解釈されるという話をいたしましたが、これとあわせますと、このようにコードを書き直すことができます。

%translation = (
  night => 'Nacht',
  morning => 'Morgen'
);
print($translation{night} . "\n"); # Nacht

スライスとレンジ演算子

配列・リストとハッシュについて説明しましたので、これらの応用としてスライスを説明します。これはPythonやRubyにも同じ概念がありますので、既に他のプログラミング言語を学ばれた方には馴染みのある概念かもしれません。

範囲や複数のindex, キーを指定して、配列・リストやハッシュから複数の値を取り出すことができます。 具体的なコードの例を見るとわかりやすいかと思います。

@animals = ('dog', 'cat', 'rabbit', 'ferret', 'parrot', 'horse', 'cow', 'pig');

@pets = @animals[0..4];
foreach $pet (@pets) {
  print("$pet "); # dog cat rabbit ferret parrot 
}

foreach $pet (@animals[0, 2]) {
  print("$pet "); # dog rabbit 
}

%translation = (
  night => 'Nacht',
  morning => 'Morgen',
  now => 'Jetzt'
);

foreach $german (@translation{'night', 'now'}) {
  print("$german "); # Nacht Jetzt 
}

複数要素を取り出した結果はリストになるのでsigilが @ になっていることに注意してください。

スライスは代入の左辺にも使うことができます

@a = (1, 2, 3, 4, 5, 6);
(@a[0..2], @a[3..5]) = (@a[3..5], @a[0..2]);
print("@a\n"); # 4 5 6 1 2 3

この i..j.. はレンジ演算子 (range operator) と言って、具体例で示した通り、指定した範囲をリストとして取り出す機能があります。なお、リストを要求するコンテクストではなくスカラーを要求するコンテクストで使うと、やや複雑ですが、式が状態を保持し、左辺の式がtrueになるまでは常にfalseを返し、一度左辺がtrueになると右辺がfalseである間は常にtrueを返す(右辺がtrueになると状態がリセットされる)というディジタル回路におけるフリップフロップのような挙動になります。 これはおそらく、ワンライナーでテキスト処理をしやすいように導入されたのではないかと思います。

というのも、スカラーのコンテクストでのレンジ演算子は、左辺と右辺が定数の数値であるときに暗黙に、現在開いているファイルの行数を保持する変数 $.== で比較する式として解釈されるからです。すなわち、 100..200 はスカラーのコンテクストでは ($. == 100) .. ($. == 200) と解釈されます。これはテキスト処理のワンライナーに便利です。例えば、10行目から20行目までを出力したい時、sedでは

$ <input.txt sed -n '10,20p'

のように書きますが、これと同じことができます

$ <input.txt perl -ne 'if (10..20) { print }'

Perlはsed以上に正規表現や文字列の変換はお手のものですので、変数や代入を省略する記法に慣れれば、短くスッキリしたワンライナーを書くことできて、ちょっとしたログの確認や分析に便利かもしれません。

スライスが配列の範囲外を刺した場合はどうなるでしょう?対応するindexの要素が存在しない場合はundefで埋められます。試してみましょう

use Data::Dumper;

@numbers = (1, 2, 3);
print(Dumper(@numbers[0..5])); # 1 2 3 undef undef

# 配列でなくリストでも同様です
print(Dumper((1, 2, 3)[0..5]));

# ハッシュでも同じです
%translation = ( night => 'Nacht', little => 'klein');
print(Dumper(@translation{'night', 'morning'})); # Nacht undef
# なお、この構文においてはbarewordは利用できません

Perlの構文の規則がそれなりに一貫して単純であることを感じられる例を挙げておきます。実は @array[i, j]@hash{key1, key2, key3} のような構文の括弧の中身はリストとして解釈されています。これを確かめる例です

use Data::Dumper;

sub f {
  0, 2
} # 呼び出すと 0, 2 というリストを返すサブルーチンです

@numbers = (1, 2, 3);
%translation = (night => 'Nacht', morning => 'Morgen', now => 'jetzt');

print(Dumber(@numbers[f])); # 1 3
print(Dumper(@translation{qw/night now/})); # Nacht jetzt

また、レンジ構文を使ったスライスはリストとして解釈されます。ですので、 @array[i, j, k, ... ]@hash{key1, key2, ...} という特別な構文があるのではなく、@array[LIST]@hash{LIST} という構文があるという理解が正しく、このように理解できていれば、以下のコードも一貫した構文の規則として理解しやすいでしょう。

@regions = ('OH', 'OR', 'VA', 'CA');
%abbr = (
  AL => 'Alabama',
  AK => 'Alaska',
  AR => 'Arizona',
  CA => 'California',
  CT => 'Connecticut',
  FL => 'Florida',
  IL => 'Illinois',
  IA => 'Iowa',
  MI => 'Michigan',
  OH => 'Ohio',
  OR => 'Oregon'
);

print(Dumper(@abbr{@regions[0..1]})); # Ohio Oregon

参照

参照はC言語で言えばポインタのようなものです。これを使うことでメモリのコピーを減らすこともできますが、それ以上に重要なのは複雑なデータ構造を表現するのに必要というところです。Perlでは配列やハッシュはスカラーしか抱えられないため、複雑なデータ構造を扱うために参照が必須となります。参照はスカラーです。C言語でポインタが単なるint型の値であることと同じことです。

既にある変数の参照を得るには、バックスラッシュ( \ )を使います。例えば、

@keywords = ('if', 'else', 'for', 'while');
$keywords_ref = \@keywords;

とすれば、変数 $keywords_ref は変数 @keywords のオブジェクトへの参照と紐付きます。ポインタがただの整数なのと同じで参照はスカラーですので、sigilが $ であることをきちんと確認しましょう。

ちなみに、参照を整数として評価させると変数のアドレスを取得できます。 以下のコードでは、算術演算の演算子によって整数として評価させていたり、あるいはprintfのフォーマット文字列によって整数として表示したりしています。

my @numbers = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
printf("%d\n", \@numbers);
foreach $number (@numbers) {
  print(\$number - 0, "\n");
}

Dereference

さて、C言語では * を使うことで参照にアクセスして、参照するオブジェクトに対して代入を行なったり値を取り出したりできます。 これをdereference(参照外し)と言いますが、Perlではdereferenceにはいくつかの方法があります。

一つはsigilを先頭に付加する方法です。 例えば、上のコードの続きを考えると、 @$keywords_ref と書くことで、参照しているオブジェクトを配列としてdereferenceします。そのため、keywords_refからforeach文ですべてのキーワードをprintで出力するコードは次のようになります。

foreach $keyword (@$keywords) {
  print($keyword);
}

また、こうしたdereferenceした配列オブジェクト @$keywords_ref の要素をindexで取り出すときは、やはり個々の要素はスカラーですのでsigilは $ となり、以下のようにかけます。

$$keywords_ref[0]

参照は別のメモリアドレスにデータをコピーしているわけではないので、参照からdereferenceして参照しているオブジェクトを書き換えると元の変数も書き変わっています。確認してみましょう。

@keywords = ('if', 'else', 'for', 'while');
$keywords_ref = \@keywords;
$$keywords_ref[0] = 'elsif';

print($$keywords_ref[0]); # elsif
print($keywords[0]); # こちらもelsifになっている

dereferenceのための次の方法は、sigilにブロック ({ statements }) を後続させる方法です。Perlではブロックの中で最後に評価した式の値がブロック全体の値として扱われることがあります。 とはいえ、

$sum = {;
  #ハッシュ参照であれば{の直後に;は絶対に含まないので、
  # ブロックを意図するときは構文解析の曖昧性を排除するために { の直後に ; をおくと間違いが少ない
  $s = 0;
  for $i (1..10) {
    $s += $i;
  }
  $s;
} #これはエラー!

これはエラーになります。これは、代入の右辺には式を書かなければならず、式を期待している文脈で剥き出しの { statements } は構文として定義されていないからです。ブロックの出現が許可されている文脈ではないからという言い方もできます。Perlでは do はループではなくブロックを評価するだけのものですので、次のように書くことはできます。

$sum = do {
  $s = 0;
  for $i (1..10) {
    $s += $i;
  }
  $s;
};

また、サブルーチンもreturnを明示的に書かずにこのように最後に評価した式の値を関数の値とすることが可能です。ここで紹介するdereferenceのブロックを使う構文でも 参照を返す という条件付きで最後の式の値を使うというルールが適用されます。

例えば、以下のようなコードは書くべきではありませんが、完全に文法としては正しく意図通りに動作します。

foreach $token (@{;
  @tokens = ();
  while (defined($line = <STDIN>)) {
    $line = chomp($line);
    push(@tokens, split(' ', $line));
  }
  \@tokens;
}) {
  print("$token\n");
}
# 慣れた人だと
# while(<STDIN>) {
#   chomp;
#   push @tokens, split ' ';
# }
# のように書くかもしれません。上で出てきた新しい要素も、このような省略も文法の基礎を学んだ後に説明します

このプログラムは標準入力からスペース、空白・改行区切りで文字列を取り込んで、それを一つ一行で出力します。何度も言いますが、これは説明のためだけに敢えて書いたコードですので、このようなコードは書くべきではないことは強調しておきます。 ただ、参照のdereferenceでブロックを使う構文は、ブロックの最後に評価される式の値(つまりはブロックが返す値)が参照であれば問題ないという説明にはなったと思います。

こういうよろしくない例だけではなく、実用的な例もお見せしますね。 Perlのstring interpolationでは変数を埋め込むことができますが、式を埋め込むことは直接はできません。 例えば、Rubyでは "#{x + 10} and #{f(x)}" , Pythonでは f"{x + 10} and {f(x)}" のように式を含めることができるのですが、Perlで "${x + 10}" のように書いてもエラーになるでしょう。

これは、 ${...} という構文が、ブロックで最後に評価された式が参照の時にスカラーとしてdereferenceする、という意味を持った構文だからです。なので、実はPerlでもブロックが参照を返すようにすることで式を文字列に埋め込むことができます。以下のコードを実行すると x + 10 = 52 と出力されます。 $x + 10 だけでは数値でスカラーですが、 \() で囲うことによって無名のオブジェクト(右辺値)に対する参照を作っていてブロックの中を評価した結果が参照になるからです。確かめてみましょう。

my $x = 52;
print("x + 10 = ${\($x + 10)}");

もちろん、変数名だけでも式としてvalidですから、 $$keyword${$keyword} と書いても大丈夫です。分かりやすいと感じる書き方をしましょう。

最後に、参照をderefernceする方法として、アロー記法を使う方法もあるので、こちらを解説します。

アロー記法によるdereferenceは具体例を見た方が早いでしょう。具体例を書きます

my @pets = ('dog', 'cat', 'rabbit');
my $pets_ref = \@pets;

print($$pets_ref[0]); # これは先ほど紹介したdereferenceの仕方
print(${$pets_ref[1]}); # これも先ほど紹介したdereferenceの仕方
print($pets_ref->[2]); # 実はアロー記法を使うと、先頭にsigilをつけずにこうやって簡単にdereferenceできる

# print($pets_ref[2]); # ちなみにこれはエラーになります

このように、先頭にsigilをつけずに、 ->[i] の形で参照している配列の要素にindexでアクセスできます。 ちなみに、最後の行でコメントアウトした $pets_ref[2] のような省略記法があると便利なのに、これはエラーになります。何故だか分かりますか?

復習になりますが、これは配列 @pets_ref の3番目 (添字は2) の要素にアクセスという意味になるからです。要素自体はスカラーなのでsigilが $ になるのでした。そして、最初の方で述べましたが、Perlではスカラーと配列・ハッシュを同名だとしても別々の領域で管理しています。なので、最後のコメントアウトされた行の文を実行しようとすると、 @pets_ref なんて配列は定義されていないというエラーになるわけです

ハッシュも同様です。具体例を書いておきますね。

my %translattion = (
  'hello' => 'Hallo',
  'morning' => 'Morgen',
  'now' => 'jetzt'
);
my $translation_ref = \%translation;

print($$translation_ref{'hello'}); # 最初に紹介したsigilを先頭にprependするdereferenceです
print(${$translation_ref}{'morning'}); # 次に紹介したブロックを使う方法です 
print($translation_ref->{'now'}); # この通りアローを使うと先頭にsigilを追加せずにアクセスできます

配列の場合と同様に、ハッシュとして参照をdereferenceするには、 %$translation_ref%{$translation_ref} となるのですが、キーを与えて値を取り出すとなると値はスカラーですから $ が必要で、よって $$translation_ref{'hello'} , ${$translation_ref}{'morning'} となるわけですね。ややこしい... アロー記法によってdereferenceすると、この辺り全く意識しないので楽ですね

アロー記法でのアクセスの良いところは、ネストした構造になっているときに、sigilをどんどんprependするのではなく次々に -> を加えていくだけで目的の要素にアクセスできるところです。これは重要なポイントですので、後述します。一旦は忘れていただき、いろんな方法があるということで、それぞれの記法に慣れていただけると良いかと思います

無名リストおよび無名ハッシュの参照

配列を作ってからバックスラッシュで参照するのではなく、無名でリストを作って、その無名オブジェクトを参照するための構文があります。 () ではなく [] を使います。具体例を見ましょう

['Google', 'Amazon', 'Facebook', 'Apple']

これで無名のリストへの参照が作られます。もちろん変数を宣言して、この参照を代入することも可能です。

$gafa = ['Google', 'Amazon', 'Facebook', 'Apple'];

print($$gafa[0]); # Google
print(${$gafa}[1]); # Amazon
print($gafa->[2]); # Facebook

細かい話をすると、実は構文としては、これも [] の中身はリストコンテクストとなっており、コンマ演算子でリストを作っています。 [expr] はその中にリストコンテクストで式をとり、その参照を作る構文なのです。

ハッシュも無名のハッシュの参照を作れます。 ( key => value, key => value ) ではなく、curly braces ({}) を使って { key => value, key => value } のように書きます。なお、ブロックではなくハッシュ参照であることを明示し、構文解析の曖昧性を減らして意図しない動作を防ぐために、 { の前に + を添える場合があります(ここでは曖昧性はないので添えていません)。具体的な例を見ていきましょう。

$translation = {
  hello => 'Hallo',
  night => 'Nacht',
  little => 'klein'
};

print($$translation{hello}); # Hallo
print(${$translation}{night}); # Nacht
print($translation->{little}); # klein

複雑なデータ構造へ

これまでに説明した参照の知識を使うことで、ネストしたリストやネストしたハッシュのような複雑な構造を表せるようになります。 配列やハッシュの要素は全てスカラーなのでした。しかし、参照もまたスカラーなので、参照を要素に持たせることはできるというわけです。

具体例を見ていきましょう。

まずは、配列の配列で二次元の表を表現するケースを考えます。

$matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

print(${$$matrix[0]}[1]); # 2 (1)
# print($$$matrix[0][1]); これはエラーになります (2)
print($matrix->[2]->[2]); # 9 (3)
print($matrix->[2][2]); # 9 この記法では最初の->は省略できないが、最初の出現より後では省略可能

(1), (3)はこれまでの知識で理解できますね。 $matrix はリストの参照を要素にもつリストの参照です。ですので、これを @$matrix とするとリストの参照を要素にもつ配列となりますね。 ですので、 $$matrix[0] はこのリスト参照を要素にもつリストの1番目の要素へのアクセスであり [1, 2, 3] というリストの参照が得られます。しかし、安易にこれにsigilの $ をprependしてから末尾に [1] をつけても、残念ながら目論見通りには解釈されません。構文上の問題でそのようには解釈されないのです。 ${$$matrix[0]}[1] または ${${$matrix}[0]}[1] と書けば意図した通りに配列の要素を取り出せるのですが、ちょっと煩わしいですね

(3)はdereferenceの方法として最後に紹介したアロー記法を使った方法です。このようにネストした構造では、sigilを前置する方法では複雑になりやすいのですが、アロー記法を使うことで後ろにアクセスしたい要素のindexをどんどん追加していくだけで良いので間違いが少なく読みやすくなりますね。最後の行の例のように、 [i][j] の間はアローが不要ですので、ちょっと規則としては分かりにくいですが慣れてくると省略することで読み書きしやすくなりますね。

次に、RESTful APIのレスポンスのJSONを配列とハッシュの参照のネストした構造として表し、それを処理するコードを考えてみます。

croak "failed to retrieve data" unless ($api_response->{status} != 200);

foreach my $repository (@{$api_response->{repositories}}) {
  my $commits = $repository->{branch}->{master}->{commits};
  # または、[]や{}の間の->は省略できるルールにより
  # my @commits = $repository->{branch}{master}{commits};
  foreach my $commit (@{$commits}) {
    print($commit->{hash});
  }
}

これも一つずつ押さえていきましょう。 $api_response はハッシュの参照です。このハッシュは status というキーを持っていて、ここにステータスコードが整数で書き込まれています。これは $api_response->{status} でアクセスできますね。アロー記法を使わない場合は例えば $$api_response{status} という書き方もできはすることを確認しておいてください。

次に、foreachのループですが、foreachの () 内はリストが要求されるコンテクストですので、リストの参照である $api_response->{repositories}@{ statement } のdereferenceの構文を合わせて @{$api_response->{repositories}} とすることで配列となりforeachループを回すことができます。

このリストから取り出した要素はハッシュの参照となっていますので、 $repository は参照です。respositoryというハッシュがbranchというキーを持ち、その値がハッシュの参照となっていて master, develop, feature1 のようなブランチ名のキーとそれに対応ずくブランチのさまざまな情報をやはりハッシュの参照として持っているというネストした構造を考えてみましょう。

このとき、masterブランチのcommitの情報を取得するには ${${${repository}{branch}}{master}}{commits} のような複雑な書き方をしたくないなら、アロー記法を使うことになります。アロー記法を使えば、 $repository->{branch}->{master}->{commits} のように複雑になりすぎずに目的のリスト(の参照)を取り出すことができます。好みの問題ですので、どちらでも良いのですが、括弧での要素へのアクセスが複数ある場合は間にあるアローを省略できるというルールはここでも適用され、 $repository->{branch}{master}{commits} と書くこともできます。

ああそうだった。もう一つ、重要な記法があるので紹介します。

@{$api_response->{repositories}} はそこまで複雑じゃないと思いますが、どうせなら @{ statement } でのdereferenceの記法を使わずに全てアロー記法で書きたくなるかもしれませんね。 アロー記法で参照を配列としてdereferenceしたりハッシュとしてdereferenceしたりすることは可能で、特別な構文が用意されています。次のような構文です。 ->@* で左辺の参照を配列としてdereferenceするという意味で、 ->%* で左辺の参照をハッシュとしてdereferenceするという意味です。なお、僕はあまり好みではありません。Perl v.5.20 (2014年) からの機能なので、古い環境では動かない可能性もあります。

foreach my $repository ($api_response->{repositories}->@*) {
  my $commits = $repository->{branch}{master}{commits};
  foreach my $commit ($commits->@*) {
    print($commit->{hash});
  }
}

具体例で示した通り、アロー記法はsigilとブレースだらけの読みにくい式にせずに済むので、ネストした構造や後程説明するオブジェクト指向ではアロー記法を使うことを推奨します。

サブルーチン

Perlでもユーザー定義の関数のようなものを作れます。subというキーワードで sub IDENTIFIER BLOCK という構文によりサブルーチンを定義し、 IDENTIFIER の名前よって呼び出せるよになります。 BLOCK はブレース {} で囲まれた文で、文はセミコロンで区切ることで複数の文を含めることができます。 BLOCK の中で最後に評価された式の値がサブルーチンの値となります。いわゆる戻り値(返り値)です。サブルーチンの呼び出しがあると、サブルーチンの中で @_ という変数を参照することで引数を配列として取り出すことができます。

具体的に、複数の数値を受け取って総和を求めるサブルーチンを定義してみますので、コードを見ながら理解を深めてください。

sub sum {
  $s = 0;
  foreach $addend (@_) {
    $s += $addend;
  }
  $s;
}

これまでに学習した知識で説明ができるはずですので、ご確認ください。他の例として、2つの文字列の引数を受け取って、第一引数の文字列が第二引数の文字列で始まっているか判定するサブルーチンを定義します。

sub startswith {
  ($subject, $pattern) = @_;
  $pattern_len = length($pattern);
  substr($subject, 0, $pattern_len) eq $pattern;
}

省略して

sub startswith {
  ($subject, $pattern) = @_;
  substr $subject, 0, length $pattern, eq $pattern;
}

とか書けないこともないですが、よしましょう。読みにくいので。 モヤモヤしてきたと思いますので、サブルーチンの説明を続けます。

先ほど定義した sum 関数ですが、文字列を与えた場合や空のリストを与えた場合どうなるでしょうね? まず、空リストの場合、引数が不足している場合の挙動を見てみましょう。この場合は、単純にforループが回らないだけで、 $s = 0 のまま $s の値が戻り値となります。問題ないですね。 次に文字列の場合ですが、 + 演算子はオペランドを数値に変換してから数値として足し合わせようとします。ですので、文字列は暗黙に数値に変換されますが、その規則は、文字列を先頭から数値として字句解析して数値と認識できたところまでを数値に変換します。 例えば '112abc' であれば 112 になり '10e2af' であれば 10e2 (1000) になります

startswithの場合、引数が足りないとどうなるでしょう?不足している場合は、不足したものはundef(未定義)になります。

startswith("Hello World");

という呼び出しの場合は、 $subject = "Hello World", $pattern = undef となります。この場合どうなるかというと、length演算子は引数がundefのとき値がundefになりますので、このサブルーチンの値(戻り値)はsubstr("Hello World", 0, 未定義の変数) eq 未定義の変数 という式の評価結果に等しくなります。この結果がどうなるか予想できるという人は相当にPerlの組み込み関数(演算子)の挙動に詳しい。まず、substrはこの場合は空文字列 ('') になります。 eq 演算子は左右のオペランドの文字列を比較しますが、空文字列と未定義の比較は、なんと 1 (true) になります。なので、このサブルーチンの第二引数を書き忘れた場合は常に true を返します。 では、全く何の引数もなかった場合はどうでしょう?実はこの場合の substr も空文字列を返します。ですので、引数を完全に忘れていると常にtrueを返す関数のように振る舞います。

で、他のプログラミング言語を利用してきた人であれば、普通は引数が足りない場合は呼び出しに失敗して欲しいのではないでしょうか。 変数が定義されているか判定する defined 演算子を使うとこれを実現できます(ただし実行時の呼び出し時に失敗します)

sub startswith {
  ($subject, $pattern) = @_;
  die '$subject not defined.' if !defined($subject); # !は論理否定
  die '$pattern not defined.' if !defined($pattern); # ifはこのように式の後ろから修飾して、実行する条件を制御できる

  substr($subject, 0, length $pattern) eq $pattern;
}

$helloworld = "Hello World";
$prefixed_with_hello = eval{ startswith($helloworld, "Hello") };
if ($@) { # evalのブロックの中でdieが呼び出されると、変数$@にエラーメッセージが代入される
  print("Failed. $@\n");
  exit(1);
}
if ($prefixed_with_hello) {
  print("'$helloworld' starts with 'Hello'\n");
}

さて、では、このstartswithサブルーチンですが、ハッシュや配列を引数として渡した場合、どのように振る舞うか確認してみましょう。

@hello = ('Hello World',  'Hello');
print(startswith(@hello)); # 1 (true)

@hello = ('Hello World',  'Halo');
print(startswith(@hello)); # (false)

配列の場合は、配列をリストとして展開したのと同じ振る舞いになるようです。ではハッシュは?実はハッシュもリストとして展開したのと同じ振る舞いになります。ただし、ハッシュは配列とは違って要素の順序が保存されることは保証されないので、予測困難な挙動になります。 残念ながら古いPerlでは呼び出されるサブルーチン側で呼び出し元が配列を引数にしたかハッシュを引数にたかは判定する手段がありませんので、きちんとサブルーチンの正しい呼び出し方をドキュメントして、ドキュメント通りに呼び出しているかチェックするしかありません。

引数が足りなかったり意図せずコンマ区切りで引数を多く取られてしまうなどの呼び出しのミスを予防する方法は一応はあります。未定義の変数を与えることもできてしまいますし配列やハッシュをスカラーを期待する引数に渡せてしまうので、完全な対策にはなりませんが、ポカ避けには十分使えます。プロトタイプ付きのサブルーチンの構文です。 sub IDENTIFIER PROTOTYPE BLOCK という構文で、 PROTOTYPE は括弧で括られた $ や @ の列です。この構文は少なくともPerl v5.10で有効です。

使いながら覚えましょう。

sub startswith ($$) {
  ($subject, $pattern) = @_;
  die '$subject undefined' unless defined($subject);
  die '$pattern undefined' unless defined($pattern);

  substr($subject, 0, length $pattern) eq $pattern
}

このように定義したstartswithサブルーチンの呼び出しは、次のような呼び出しではコンパイル時エラーになります。なお、もしかしたら、Perlでコンパイルというと「え」と思ったかもしれませんが、PerlはVMが実装されており、コードをそのVMの上で動くバイトコードにコンパイルしながら動いています。

# 以下、全てコンパイル時エラー
startswith; # 引数が足りない
startswith("Hello"); # 引数が足りない
startswith("Hello World", "Hello", "Hey"); # 引数が多すぎる
startswith(%hash); # 引数が足りない

ちなみに、 startswith(@array, "Hello") はエラーになりません。これはどういう挙動になるかというと、あたかも

$temp1 = @array;
$temp2 = "Hello";
@_ = ($temp1, $temp2);

と書いたかのような挙動になり、 $subject には @array の要素数が代入されます。スカラーの引数にハッシュを与えた場合も同様に(Perlのバージョンによって挙動が異なりますが現在のv5.38では)ハッシュのキーの個数が代入されます。こうしたバグは現状のPerlでは言語機能で防ぐことはできません。ですので、やはりAPIドキュメントはきちんと作成するようにして、呼び出し側で誤った呼び出し方をしないように気をつけるとともに、きちんと自動テストを書いて振る舞いが期待通りであることを確認しながらコードを書いていくことが望ましいです。

プロトタイプを使う場合も使わない場合も注意すべき点としては、意図しないバグを防ぐには、以前説明した関数(list operatorとnamed unary operator)で説明したのと同様に、引数を括弧で括るべきだということです。 サブルーチンは後続するリストを、リストがコンマ演算子で続く限りは全て引数として吸い上げてしまいます。プロトタイプで参照を含むスカラーのみ要求する場合は引数の数が合わないとエラーを出力するのでバグに気付けますが、そうでないとバグになかなか気付けないこともあります。

例えば、2つの数値を足し合わせる add というサブルーチンを定義したとしましょう

sub add {
  ($x, $y) = @_;
  $x + $y

  # Note: サブルーチンの呼び出しの式を評価した結果は、サブルーチンのブロックの中で最後に評価した式の値
  # なのでreturnは不要。明示しても良いが、guard clauseのためのearly return以外では本文書の著者は使わない
  # 好みの問題でもあるが、開発の現場では一貫性があることが大切なので、コーディング規約でどちらのスタイルにするか明示すると良い
}

print "1 + 2 = ", add 1, 2, "\n";

これは何を出力するか予測できますか? なんとなくですが、x, yの二つの引数を足し合わせるサブルーチンですので、 "1 + 2 = 3\n" と出力することを期待してしまいますよね。ところが、そうはなりません。コンマ演算子が演算子の優先度の都合で途切れない限りは、リストは続き、その全てを @_ に束縛して呼び出すような挙動になるので、このコードは以下のコードと同じ結果が得られます。

(print "1 + 2 = ", (add 1, 2, "\n"));

そして、これは、 (1, 2, "\n") というリストを @_ に代入してサブルーチンのコードを実行するような振る舞いになりますので、引数で使用されなかった "\n は単に捨てられることになります。 よって、

(print "1 + 2 = ", (add 1, 2));

というコードと実質的に同じ動作になります。

他のプログラミング言語の関数呼び出しと同じように括弧を付けることで、このような意図しないバグを防げます。

print("1 + 2 = ", add(1, 2), "\n");

この例のように、他のプログラミング言語で見られるように括弧で引数をきちんと括って書くことを強くお勧めします。

伝統的なPerlプログラムでは、サブルーチンが引数を受け取る方法は、 @_ を使う他なかったのですが、内部的にはおそらく同じことをしているが Perl v5.36 以上のバージョンのsyntax sugarとして signatures という機能が実験的に導入されています。 これを使うと、他の多くのプログラミング言語の関数と似た構文でサブルーチンをかくこサブルーチンを書くことができます。

use feature 'signagures';

suh add ($x, $y) {
  $x + $y
}

サブルーチンの名前の後ろに () が出現するのは構文としてプロトタイプと被っていますので、この機能を利用する場合はプロトタイプは併用できません...と言いたいところですが、signaturesを有効にしたときには併用するための新しい構文が利用できます。次のようにプロトタイプを書けます。

use feature 'signatures';

sub add :prototype($$) ($x, $y) {
  $x + Xy
}

プロトタイプに話を戻して、もう少し例を追加します。 プロトタイプではスカラー以外に配列、配列の参照、サブルーチンの参照、ハッシュ、ハッシュの参照を引数の型として指定できます。また、必須の引数とオプショナルな引数を指定することもできます。これらの具体例を見ていきましょう。

配列を引数の型とする場合、呼び出し側では配列やリストを渡すことができます。

sub sum (@) {
  $augend = 0;
  foreach $addend (@_) {
    $augend += $addend;
  }
  $augend;
}

または

use feature 'signatures';

sub sum :prototype(@) (@addends) {
  $augend = 0;
  foreach $addend (@addends) {
    $augend += $addend;
  }
  $augend;
}

このようにサブルーチンを定義すると、配列やリストを受け取って全て数値として(数値でない場合は数値に変換される)足し合わせます。 また次のように一つ目の引数はスカラーであると型を指定してjoin関数を書くこともできます

sub join_char_strings ($@) {
  ($sep, @strs) = @_;
  $joined = '';
  for ($i = 0; $i < $#strs; $i++) {
    $joined .= "$strs[$i]$sep";
  }
  "$joined$strs[$#strs]"
}

または

use feature 'signatures';

sub join_char_strings :prototype($@) ($sep, @strs) {
  $joined = '';
  for ($i = 0; $i < $#strs; $i++) {
    $joined .= "$strs[$i]$sep";
  }
  "$joined$strs[$#strs]"
}

プロトタイプを使わない場合でも同じ注意が必要なのですが、配列・リストの引数は必ずサブルーチンの最後の引数としてください。また、この型の引数は複数取ることはできないことに注意してください。

配列・リストの説明の際にも述べた通り、

($a, @a, @b) = (1, 2, 3, 4, 5);

のように書くと、まずは $a = 1 となるが、配列 @a の長さの上限がないためリストの末尾まで @a に「食われる」ような挙動となるからです。@b が未定義になるからです。ですので、引数に配列・リストを取る場合は、引数の最後にする必要があり、かつ複数取ることはできません。ただし、配列またはリストの参照であればスカラーですので複数取ることができます。

プロトタイプを使うことで実現できる機能として、配列を実引数に取りつつ、サブルーチンの中では配列の参照として扱える機能があります。プロトタイプの宣言の中で \@ というバックスラッシュ付きの @ を使います。これを使うことで、組み込み関数のpushのように配列を引数として受け取って、その配列に対して要素を追加したり書き換えたりするなど副作用を持ったサブルーチンを実装できます。

sub add (\@$) {
  ($array_ref, $addend) = @_;
  foreach $elem (@$array_ref) {
    $elem += $addend;
  }
  @$array_ref;
}

@array = (1, 2, 3, 4);
add(@array, 10); # @array = (11, 12, 13, 14)

サブルーチンの参照を受け取りたい場合は、 & を使います。これにより、例えば map 関数のようなサブルーチンも実装できます。

sub mymmap (&\@) {
  ($fn, $aref) = @_;
  @result = ();
  foreach $elem (@$aref) {
    push(@result, &fn($elem));
  }
  @result;
}

@array = (1, 2, 3);
@array = mymap(sub { $_[0] * $_[0] }, @array);
print("@array\n"); # 1 4 9

配列だけでなく、リスト参照も受け取れるようにするには、 \@ ではなく + を使います

sub mymap (&+) {
  ($fn, $aref) = @_;
  $result = ();
  foreach $elem (@$aref) {
    push(@result, &$fn($elem));
  }
  @result;
}

@array = mymap(sub { $_[0] * $_[0] }, [1, 2, 3, 4]);
print("@array\n"); # 1 4 9 16

必須の引数以外にオプショナルの引数を取るサブルーチンを定義するには、 ; を使います。 ; の前は必須の引数の型、後はオプショナルの変数の型のプロトタイプとして扱われます。

変数のスコープ

次回の更新で書きたい いい加減、 my を使いたい

パッケージ

次回の更新で書きたい

blessとオブジェクト指向プログラミング

次回の更新で書きたい

doとeval

次回のまた次の更新で書きたい

@INCとモジュールの読み込み

次回のまた次の更新で書きたい

ファイルハンドル

モジュール読み込みまで書いた後に書きたい

typeglob

書かないかもしれない

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment