地獄のPerlを解説する

# 発端

下記ツイートにより、地獄のPerlが見つかりました。

<blockquote class="twitter-tweet" data-theme="dark"><p lang="en" dir="ltr">from the Animal Crossing build scripts, I give you: the perl from hell <a href="https://t.co/QwBsKO27Rs">pic.twitter.com/QwBsKO27Rs</a></p>&mdash; badidea 💫 (@0xabad1dea) <a href="https://twitter.com/0xabad1dea/status/1287447737483624450?ref_src=twsrc%5Etfw">July 26, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

```pl:地獄のPerl
while(<>) {
if (($_ =~ /\#/)
    || ($_ =~ /=/)
    || ($_ =~ /\* Total/)
    || ($_ =~ />>>/)) {
    if ($_ =~ /\#/) {
    print $_;
    }
```

そこで、地獄ツアーと題し、上記スクリプトを解説しようと思います。



# 解説

ようこそ、地獄ツアーへ


## インデントを揃える

正直、読みづらいです。
それは、インデントがぐちゃぐちゃだからですね！
直しましょう。

```pl:インデント調整版
while(<>) {
    if (($_ =~ /\#/)
            || ($_ =~ /=/)
            || ($_ =~ /\* Total/)
            || ($_ =~ />>>/)) {
        if ($_ =~ /\#/) {
            print $_;
        }
```

そうすると、中括弧が足りないことに気づくはずです。
付け足しましょう。

```pl:中括弧追加版
while(<>) {
    if (($_ =~ /\#/)
            || ($_ =~ /=/)
            || ($_ =~ /\* Total/)
            || ($_ =~ />>>/)) {
        if ($_ =~ /\#/) {
            print $_;
        }
        # 以降の処理
    }
}
```


## 各要素を解説する

以降、Perlを知らない人向けの要素説明をします。
Perlは、動作を如何様にも変更できるため~~（演算子の元の意味なんて飾りです）~~、既定の動作のみ説明します。

### `<>`

while文の評価値として `<>` があります。

これは、ダイアモンド演算子です。
ファイル読込みや標準出力読込みを簡単にする際などに利用します。

while文の評価値にダイアモンド演算子がある場合、ファイルを一行ごと読込んで処理できます。

[行入力演算子「<>」 - ファイルから一行読み込む - Perlゼミ](https://tutorial.perlzemi.com/blog/20080722121673.html)

ちなみに、while文で取得できる一行の値がどこに入るかと言うと、 `$_` に入ります。

### `$_`

様々な箇所で登場していますね。

これは、デフォルト変数と呼ばれるものです。

Perlでは、評価値を受取る変数を定義しない場合、デフォルト変数に値が入る仕組みになっています。
また、関数によっては、引数を指定しない場合にデフォルト変数を呼出すような作りになっているものがあります。

[デフォルト変数 $_ - Perlゼミ](https://tutorial.perlzemi.com/blog/20180118151628.html)

アンダースコア1つだけの変数は、PythonやRustでもよく登場しますし、そこまで毛嫌いするものでもなくなってきた印象があります[^アンダースコア]。

[^アンダースコア]: 最も、PythonやRustのアンダースコアは、戻り値を無視するような使い方がメインですが......

~~変数名を考えるのが面倒という方にはオススメです！~~

### `=~`

if文内でたくさん使っていますね。

これは、パターンマッチ演算子と呼ばれるものです。
その名の通り、文字列のパターンマッチを評価します。

正規表現と一緒に利用します。

[「=~」 - パターンマッチ演算子 - Perlゼミ](https://tutorial.perlzemi.com/blog/20080331120697.html)

### `/ ... /`

if文内で、パターンマッチ演算子と一緒に使っていますね。

これは、正規表現を表します。
文字列の出現パターンを表現できる、すごいツールです。

[Perlの正規表現をマスターしよう - Perlゼミ](https://tutorial.perlzemi.com/blog/20100827127859.html)

正規表現内の意味については、下記にざっと記します。

- `/\#/`: `#` 文字が存在する。 ex) `#hoge #fuga`
- `/=/`: `=` 文字が存在する。 ex) `=`
- `/\* Total/`: `* Total` 文字列が存在する。 ex) `* Total: 100`
- `/>>>/`: `>>>` 文字列が存在する。 ex) `Perl >>> xxxx`

正規表現は、それだけで1冊の本になるくらい奥が深いので、知ってみると面白いですよ。

[O'Reilly Japan - 詳説 正規表現 第3版](https://www.oreilly.co.jp/books/9784873113593/)

ちなみに、対象変数を指定しない場合は `$_` を評価します。

### `print`

特に説明することでもないと思いますが......

標準出力に文字列を出力する組込み関数です。
ほとんどのプログラミング言語には搭載されている機能でもあります。

Perlは、関数を呼出す際に `()` を使わなくても良いという文法です（Python2のprintでもそうでしたね）。
そのため、 `print $foo` と `print($foo)` は、どちらも同じ動作です。

[print関数 - 文字列を出力する - Perlゼミ](https://tutorial.perlzemi.com/blog/20170404149131.html)

ちなみに、対象変数を指定しない場合は `$_` を評価します。


## 解説まとめ

ここまで見たら、内容が見やすくなっているはずです。
コメントを追加してみました。

```pl:中括弧追加版
# ファイルの各行ごとに処理をする
while(<>) { # $_ に1行の文字列が入る
    if (($_ =~ /\#/) # もし $_ 文字列に '#' を含んでいるなら
            || ($_ =~ /=/) # または、 $_ 文字列に '=' を含んでいるなら
            || ($_ =~ /\* Total/) # または、 $_ 文字列に '* Total' を含んでいるなら
            || ($_ =~ />>>/)) { # または、 $_ 文字列に '>>>' を含んでいるなら
        if ($_ =~ /\#/) { # もし $_ 文字列に '#' を含んでいるなら
            print $_; # $_ を標準出力する
        }
        # 以降の処理
    }
}
```

もう読めますね。
Perlの可読性が低いなんて、やっぱり嘘なんです！



# もっと綺麗にしてみる

せっかくなので、元のソースコードを、もう少し綺麗にしてみましょう。

```pl:元のソースコード
while(<>) {
if (($_ =~ /\#/)
    || ($_ =~ /=/)
    || ($_ =~ /\* Total/)
    || ($_ =~ />>>/)) {
    if ($_ =~ /\#/) {
    print $_;
    }
```


## インデントを揃える

既に対応しましたが、それは可読性が段違いだからです。
これは、どのプログラミング言語でも一緒ですよね。


## 中括弧を付ける

既に対応しましたが、これは以降の説明をしやすくするための処置です。
ソースコードの可読性の観点からは、特に関係はありません。

```pl:中括弧追加版
while(<>) {
    if (($_ =~ /\#/)
            || ($_ =~ /=/)
            || ($_ =~ /\* Total/)
            || ($_ =~ />>>/)) {
        if ($_ =~ /\#/) {
            print $_;
        }
        # 以降の処理
    }
}
```


## `$_` を消す

既に書きましたが、 `=~`（パターンマッチ演算子）および `print` 関数は、対応する変数がない場合は `$_` を呼出します。
そのため、わざわざ `$_` を呼出す必要性はありません。

せっかく最初から使っていないなら、消しちゃいましょう。

```pl:$_除去版
while(<>) {
    if ((/\#/)
            || (/=/)
            || (/\* Total/)
            || (/>>>/)) {
        if (/\#/) {
            print;
        }
        # 以降の処理
    }
}
```


## 外枠のif文評価値内で利用している `()` を消す

`$_` を消したため、if文の評価値として使っている `()` は無くても問題ありませんね[^カッコの必要性]。
消しちゃいましょう。

[^カッコの必要性]: 動作としては、最初から必要ありません。 [`=~` の方が `||` より優先順位が高い](https://www.javadrive.jp/perl/ope/index10.html)ためです。しかし、 `$_ =~ / ... /` をカッコで括った方が可読性は良いですし、演算子の優先順位を常に考える必要性もありません（保守性も良い）ので、もし `=~` 演算子を使うのであれば、カッコはあると良いでしょう。

```pl:カッコ除去版
while(<>) {
    if (/\#/
            || /=/
            || /\* Total/
            || />>>/) {
        if (/\#/) {
            print;
        }
        # 以降の処理
    }
}
```


## `||` を `or` に置換する

`||` と `or` の意味は同じです。
演算子の優先順位は違います（`or` の方が低いです）が、これくらいなら置換えた方が見やすいです。

```pl:or置換版
while(<>) {
    if (/\#/
            or /=/
            or /\* Total/
            or />>>/) {
        if (/\#/) {
            print;
        }
        # 以降の処理
    }
}
```


## ４つの正規表現を変数化する

正規表現は便利ですが、何を意味しているのかがわかりづらいのが欠点です。
そこで、変数化することでわかりやすくなります。

また、 `#` については、調べていくうちにエスケープシーケンスである必要は無いことがわかりました。
`\#` から `#` に変更しても意味が変化しないため、エスケープ文字を外します。

正規表現の意味は不明ですが、とりあえず下記のような、そのまんまな感じで書きました。

```pl:正規表現変数化版
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における正規表現](https://www.javadrive.jp/perl/regex/var/index1.html)


## 外枠のif文評価値内で利用している4つの正規表現を1つにまとめる

if文で使っている `or` は、正規表現内の `|` と同じような意味で使えます。
行数も多いですし、置換しちゃいましょう。

```pl:正規表現一元化版
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行にまとめられます。
ここまでするかは議論が分かれるところではありますが、僕は好きなので、これくらいであれば使っちゃいます。

```pl:後置のif版
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は、早期リターンのような使い方をする際に便利です。
ご存知なかった方は、ぜひ使ってみてください。

[if修飾子 - 後置のif - Perlゼミ](https://tutorial.perlzemi.com/blog/20080324120637.html)



# おわりに

修正前と修正後を比べます。

```pl:修正前
while(<>) {
if (($_ =~ /\#/)
    || ($_ =~ /=/)
    || ($_ =~ /\* Total/)
    || ($_ =~ />>>/)) {
    if ($_ =~ /\#/) {
    print $_;
    }
```

```pl:修正後
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つ証明されてしまいました。