発端
下記ツイートにより、地獄のPerlが見つかりました。
from the Animal Crossing build scripts, I give you: the perl from hell pic.twitter.com/QwBsKO27Rs
— badidea 💫 (@0xabad1dea) July 26, 2020
while(<>) {
if (($_ =~ /\#/)
|| ($_ =~ /=/)
|| ($_ =~ /\* Total/)
|| ($_ =~ />>>/)) {
if ($_ =~ /\#/) {
print $_;
}
そこで、地獄ツアーと題し、上記スクリプトを解説しようと思います。
解説
ようこそ、地獄ツアーへ
インデントを揃える
正直、読みづらいです。
それは、インデントがぐちゃぐちゃだからですね!
直しましょう。
while(<>) {
if (($_ =~ /\#/)
|| ($_ =~ /=/)
|| ($_ =~ /\* Total/)
|| ($_ =~ />>>/)) {
if ($_ =~ /\#/) {
print $_;
}
そうすると、中括弧が足りないことに気づくはずです。
付け足しましょう。
while(<>) {
if (($_ =~ /\#/)
|| ($_ =~ /=/)
|| ($_ =~ /\* Total/)
|| ($_ =~ />>>/)) {
if ($_ =~ /\#/) {
print $_;
}
# 以降の処理
}
}
各要素を解説する
以降、Perlを知らない人向けの要素説明をします。
Perlは、動作を如何様にも変更できるため~~(演算子の元の意味なんて飾りです)~~、既定の動作のみ説明します。
<>
while文の評価値として <>
があります。
これは、ダイアモンド演算子です。
ファイル読込みや標準出力読込みを簡単にする際などに利用します。
while文の評価値にダイアモンド演算子がある場合、ファイルを一行ごと読込んで処理できます。
行入力演算子「<>」 - ファイルから一行読み込む - Perlゼミ
ちなみに、while文で取得できる一行の値がどこに入るかと言うと、 $_
に入ります。
$_
様々な箇所で登場していますね。
これは、デフォルト変数と呼ばれるものです。
Perlでは、評価値を受取る変数を定義しない場合、デフォルト変数に値が入る仕組みになっています。
また、関数によっては、引数を指定しない場合にデフォルト変数を呼出すような作りになっているものがあります。
アンダースコア1つだけの変数は、PythonやRustでもよく登場しますし、そこまで毛嫌いするものでもなくなってきた印象があります1。
変数名を考えるのが面倒という方にはオススメです!
=~
if文内でたくさん使っていますね。
これは、パターンマッチ演算子と呼ばれるものです。
その名の通り、文字列のパターンマッチを評価します。
正規表現と一緒に利用します。
/ ... /
if文内で、パターンマッチ演算子と一緒に使っていますね。
これは、正規表現を表します。
文字列の出現パターンを表現できる、すごいツールです。
正規表現内の意味については、下記にざっと記します。
-
/\#/
:#
文字が存在する。 ex)#hoge #fuga
-
/=/
:=
文字が存在する。 ex)=
-
/\* Total/
:* Total
文字列が存在する。 ex)* Total: 100
-
/>>>/
:>>>
文字列が存在する。 ex)Perl >>> xxxx
正規表現は、それだけで1冊の本になるくらい奥が深いので、知ってみると面白いですよ。
ちなみに、対象変数を指定しない場合は $_
を評価します。
print
特に説明することでもないと思いますが......
標準出力に文字列を出力する組込み関数です。
ほとんどのプログラミング言語には搭載されている機能でもあります。
Perlは、関数を呼出す際に ()
を使わなくても良いという文法です(Python2のprintでもそうでしたね)。
そのため、 print $foo
と print($foo)
は、どちらも同じ動作です。
ちなみに、対象変数を指定しない場合は $_
を評価します。
解説まとめ
ここまで見たら、内容が見やすくなっているはずです。
コメントを追加してみました。
# ファイルの各行ごとに処理をする
while(<>) { # $_ に1行の文字列が入る
if (($_ =~ /\#/) # もし $_ 文字列に '#' を含んでいるなら
|| ($_ =~ /=/) # または、 $_ 文字列に '=' を含んでいるなら
|| ($_ =~ /\* Total/) # または、 $_ 文字列に '* Total' を含んでいるなら
|| ($_ =~ />>>/)) { # または、 $_ 文字列に '>>>' を含んでいるなら
if ($_ =~ /\#/) { # もし $_ 文字列に '#' を含んでいるなら
print $_; # $_ を標準出力する
}
# 以降の処理
}
}
もう読めますね。
Perlの可読性が低いなんて、やっぱり嘘なんです!
もっと綺麗にしてみる
せっかくなので、元のソースコードを、もう少し綺麗にしてみましょう。
while(<>) {
if (($_ =~ /\#/)
|| ($_ =~ /=/)
|| ($_ =~ /\* Total/)
|| ($_ =~ />>>/)) {
if ($_ =~ /\#/) {
print $_;
}
インデントを揃える
既に対応しましたが、それは可読性が段違いだからです。
これは、どのプログラミング言語でも一緒ですよね。
中括弧を付ける
既に対応しましたが、これは以降の説明をしやすくするための処置です。
ソースコードの可読性の観点からは、特に関係はありません。
while(<>) {
if (($_ =~ /\#/)
|| ($_ =~ /=/)
|| ($_ =~ /\* Total/)
|| ($_ =~ />>>/)) {
if ($_ =~ /\#/) {
print $_;
}
# 以降の処理
}
}
$_
を消す
既に書きましたが、 =~
(パターンマッチ演算子)および print
関数は、対応する変数がない場合は $_
を呼出します。
そのため、わざわざ $_
を呼出す必要性はありません。
せっかく最初から使っていないなら、消しちゃいましょう。
while(<>) {
if ((/\#/)
|| (/=/)
|| (/\* Total/)
|| (/>>>/)) {
if (/\#/) {
print;
}
# 以降の処理
}
}
外枠のif文評価値内で利用している ()
を消す
$_
を消したため、if文の評価値として使っている ()
は無くても問題ありませんね2。
消しちゃいましょう。
while(<>) {
if (/\#/
|| /=/
|| /\* Total/
|| />>>/) {
if (/\#/) {
print;
}
# 以降の処理
}
}
||
を or
に置換する
||
と or
の意味は同じです。
演算子の優先順位は違います(or
の方が低いです)が、これくらいなら置換えた方が見やすいです。
while(<>) {
if (/\#/
or /=/
or /\* Total/
or />>>/) {
if (/\#/) {
print;
}
# 以降の処理
}
}
4つの正規表現を変数化する
正規表現は便利ですが、何を意味しているのかがわかりづらいのが欠点です。
そこで、変数化することでわかりやすくなります。
また、 #
については、調べていくうちにエスケープシーケンスである必要は無いことがわかりました。
\#
から #
に変更しても意味が変化しないため、エスケープ文字を外します。
正規表現の意味は不明ですが、とりあえず下記のような、そのまんまな感じで書きました。
my $has_hash = '#';
my $has_equal = '=';
my $has_total = '\* Total';
my $has_triple_gt = '>>>';
while(<>) {
if (/$has_hash/
or /$has_equal/
or /$has_total/
or /$has_triple_gt/) {
if (/$has_hash/) {
print;
}
# 以降の処理
}
}
どうでしょう、ここに来て急に読みやすくなったのではないでしょうか?
変数内の文字列をパターンマッチング判定として使う場合、変数を /
で囲む必要があります。
変数の展開 - パターンへの変数展開 - Perlにおける正規表現
外枠のif文評価値内で利用している4つの正規表現を1つにまとめる
if文で使っている or
は、正規表現内の |
と同じような意味で使えます。
行数も多いですし、置換しちゃいましょう。
my $has_hash = '#';
my $has_equal = '=';
my $has_total = '\* Total';
my $has_triple_gt = '>>>';
while(<>) {
if (/$has_hash|$has_equal|$has_total|$has_triple_gt/) {
if (/$has_hash/) {
print;
}
# 以降の処理
}
}
内枠のif文を1行にする
後置のifを使うことで、if文を1行にまとめられます。
ここまでするかは議論が分かれるところではありますが、僕は好きなので、これくらいであれば使っちゃいます。
my $has_hash = '#';
my $has_equal = '=';
my $has_total = '\* Total';
my $has_triple_gt = '>>>';
while(<>) {
if (/$has_hash|$has_equal|$has_total|$has_triple_gt/) {
print if /$has_hash/;
# 以降の処理
}
}
後置のifは、早期リターンのような使い方をする際に便利です。
ご存知なかった方は、ぜひ使ってみてください。
おわりに
修正前と修正後を比べます。
while(<>) {
if (($_ =~ /\#/)
|| ($_ =~ /=/)
|| ($_ =~ /\* Total/)
|| ($_ =~ />>>/)) {
if ($_ =~ /\#/) {
print $_;
}
my $has_hash = '#';
my $has_equal = '=';
my $has_total = '\* Total';
my $has_triple_gt = '>>>';
while(<>) {
if (/$has_hash|$has_equal|$has_total|$has_triple_gt/) {
print if /$has_hash/;
# 以降の処理
}
}
可読性が、すごく高くなっていることがわかります。
Perlは可読性が悪いわけじゃ無いということが、また1つ証明されてしまいました。
-
最も、PythonやRustのアンダースコアは、戻り値を無視するような使い方がメインですが...... ↩
-
動作としては、最初から必要ありません。
=~
の方が||
より優先順位が高いためです。しかし、$_ =~ / ... /
をカッコで括った方が可読性は良いですし、演算子の優先順位を常に考える必要性もありません(保守性も良い)ので、もし=~
演算子を使うのであれば、カッコはあると良いでしょう。 ↩