元ネタ
色んな言語でidol++して「大石泉すき」と出力する
https://qiita.com/m19e/items/8c7e4a0f3bbef4715264
Perlがなかったので、折角なのでできるだけ短いコードでできるように頑張った。
レギュレーション
変数 idol = 0 を izumi = 1111 に到達するまで idol++ して結果を出力します。
出力が一行だと寂しいので100の倍数でも出力しています。
解釈
- 次のことを順番に行わなければならない
- 変数の宣言と初期化
- idolという変数を必要であれば宣言し、必要であれば0もしくはそれに準する値で初期化しなければならない
- izumiという変数を必要であれば宣言し、1111という値で初期化しなければならない
- インクリメント
- 変数idolの値を1増加させなければならない(idol++)
- idolがizumiになるまで、この処理をループや再起呼び出し等により繰り返さなければならない
- idolが100の倍数かつ0以外のとき、それを所定のフォーマットで出力しなければならない
- 出力
- ループを終えたらidolの値を所定のフォーマットで出力しなければならない
- 変数の宣言と初期化
頑張った結果
$ perl -E'$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;say"$idol到達! 大石泉すき"'
100
200
300
400
500
600
700
800
900
1000
1100
1111到達! 大石泉すき
戦略
最も基本となるコードは次の通り。
$idol = 0;
$izumi = 1111;
while (1) {
if ($idol == $izumi) {
last;
}
if ($idol != 0 && $idol % 100 == 0) {
print $idol."\n";
}
$idol++;
}
print $idol."到達! 大石泉すき\n";
2個の変数を初期化して、無限ループに入って、idolがizumiだったときに抜けて、そうじゃなければidolが特定の値の時に出力して、idolをインクリメントして、ループを抜けたら特定の文字列とともに出力するだけです。
perlではbreakをlastと書きます。
.は文字列結合の演算子です。
以下では、これをもとに徐々に短くできる部分を短く改変していきます。視認性のために、できるだけ細かい部分から順に改変します。
条件式の非ゼロ判定を消す
$idol = 0;
$izumi = 1111;
while (1) {
if ($idol == $izumi) {
last;
}
if ($idol && $idol % 100 == 0) {
print $idol."\n";
}
$idol++;
}
print $idol."到達! 大石泉すき\n";
整数を論理値として評価すると、0の時に偽で0以外の時に真になります。ここでは!= 0は省略できることになります。
一致演算を減算にする
$idol = 0;
$izumi = 1111;
while (1) {
unless ($idol - $izumi) {
last;
}
if ($idol && $idol % 100 == 0) {
print $idol."\n";
}
$idol++;
}
print $idol."到達! 大石泉すき\n";
if ($idol == $izumi)はunless ($idol - $izumi)と書けます。$idol - $izumiは値が等しい時に0(≒偽)になるので、$idol - $izumiが偽である判定で値が一致していることを判定できます。unlessはifの逆の分岐を行います。
条件分岐を1行にまとめる
$idol = 0;
$izumi = 1111;
while (1) {
$idol - $izumi || last;
($idol && $idol % 100 == 0) && print $idol."\n";
$idol++;
}
print $idol."到達! 大石泉すき\n";
||は、左辺が偽だった場合にのみ右辺を評価します。unlessは条件式が偽だった場合のみ本文を実行するので、同じことができます。&&も同様にifと同等の働きがあります。
sayを使う
use v5.10;
$idol = 0;
$izumi = 1111;
while (1) {
$idol - $izumi || last;
($idol && $idol % 100 == 0) && say $idol;
$idol++;
}
say $idol."到達! 大石泉すき";
sayは改行付きの出力を行います。sayを使うにはuse v5.10;宣言が必要です。
中間報告処理の最適化
use v5.10;
$idol = 0;
$izumi = 1111;
while (1) {
$idol - $izumi || last;
$idol % 100 || $idol && say $idol;
$idol++;
}
say $idol."到達! 大石泉すき";
次のような等価変換を行います。
($idol && $idol % 100 == 0) && say $idol;
括弧を取り除いても計算順は変わらないため、
$idol && $idol % 100 == 0 && say $idol;
判定の順序を入れ替えても変わらないため、
$idol % 100 == 0 && $idol && say $idol;
条件式を否定して&&を||にしても変わらないため、
$idol % 100 != 0 || $idol && say $idol;
!= 0は省略できるため、
$idol % 100 || $idol && say $idol;
評価順序を入れ替える理由は&&の方が||よりも結合優先度が高いため、
($idol && $idol % 100) || say $idol;
のように判定されるためです。この場合、$idolが0の時にも表示が行われるようになります。
判定を正規表現に
use v5.10;
$idol = 0;
$izumi = 1111;
while (1) {
$idol - $izumi || last;
$idol =~ /00$/ && say $idol;
$idol++;
}
say $idol."到達! 大石泉すき";
よく考えてみれば、0以外の100の倍数と言うのは、その数の右端に00という数字列が来ることと等価です。それは正規表現によって簡潔に書けます。
ループの条件式を埋め込む
use v5.10;
$idol = 0;
$izumi = 1111;
while ($idol - $izumi) {
$idol =~ /00$/ && say $idol;
$idol++;
}
say $idol."到達! 大石泉すき";
$idol - $izumi || last;について、条件式が偽なら抜けるというのは、真の場合にのみループを続けることと同じです。whileの条件式に埋め込むことができます。
idolを短縮
use v5.10;
$idol = 0;
$izumi = 1111;
for ($idol) {
while ($_ - $izumi) {
$_ =~ /00$/ && say $_;
$_++;
}
}
say $idol."到達! 大石泉すき";
for (配列) { 本文 }は、配列の要素1個1個に対して本文を実行する構文です。処理中の配列の要素は$_で参照できます。また、$_に代入等を行うと、それは配列の要素に対して影響を与えます。
forは別に配列じゃなくても使えます。$idolを与えると、要素がそれ1個で構成された配列として認識されるので$_で$idolと同じ実体を表せるようになります。
$idolへの読み込み書き込みを$_へのそれにすることで文字数を節約できます。実際に$idolの中身が書き換わっているので、ループを抜けた後に$idolを参照すると、ちゃんと書き換わった状態になっています。
$_の省略
use v5.10;
$idol = 0;
$izumi = 1111;
for ($idol) {
while ($_ - $izumi) {
/00$/ && say;
$_++;
}
}
say $idol."到達! 大石泉すき";
perlでは、対象を省略すると$_が指定されたことになる文法が多く存在します。sayの引数や正規表現の比較対象を省略すると、$_を指定したことと同じ働きをします。
カンマ大作戦
use v5.10;
$idol = 0;
$izumi = 1111;
for ($idol) {
while ($_ - $izumi) {
/00$/ && say, $_++;
}
}
say $idol."到達! 大石泉すき";
式を,で区切ると左から順に評価されます。/00$/ && sayも$_++も式であるため、まとめられます。
1行形式大作戦
use v5.10;
$idol = 0;
$izumi = 1111;
for ($idol) {
/00$/ && say, $_++ while $_ - $izumi;
}
say $idol."到達! 大石泉すき";
while (条件式) { 本文 }は、本文が1行の式であれば本文 while 条件式と書くことができます。これはifやforなどいくつかの制御構文でも同様です。
初期化の省略
use v5.10;
$izumi = 1111;
for ($idol) {
/00$/ && say, $_++ while $_ - $izumi;
}
say $idol."到達! 大石泉すき";
perlでは初期化していない変数はundefになっており、偽や0と同じ働きをします。
変数の文字列中の埋め込み
use v5.10;
$izumi = 1111;
for ($idol) {
/00$/ && say, $_++ while $_ - $izumi;
}
say "$idol到達! 大石泉すき";
"で囲われた文字列リテラルの中では変数を埋め込むことができます。到の字がたまたま特殊だったため、直前のlまでで変数名判定が終わりました。
1x4大作戦
use v5.10;
$izumi = 1x4;
for ($idol) {
/00$/ && say, $_++ while $_ - $izumi;
}
say "$idol到達! 大石泉すき";
x演算子は、左辺の文字列を右辺の回数文繰り返した文字列を返します。1を4回繰り返すと1111になるため、1文字短くなります。
forをmapに
use v5.10;
$izumi = 1x4;
map {
/00$/ && say, $_++ while $_ - $izumi;
} $idol;
say "$idol到達! 大石泉すき";
map { 本文 } 配列は、配列の要素1個1個に対して本文を評価し、その結果を配列にまとめ直して返す式です。細部は別物ですが、ここではforと実質同じ動きをします。
省略可能なセミコロンを削除
use v5.10;
$izumi = 1x4;
map {
/00$/ && say, $_++ while $_ - $izumi
} $idol;
say "$idol到達! 大石泉すき"
;は文と文を区切ることができるというだけで、実は必須ではありません。
空白文字を削除
use v5.10;$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;say"$idol到達! 大石泉すき"
ぴったりくっつけられるものは削っていきます。
ワンライナー形式にする
$ perl -e 'use v5.10;$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;say"$idol到達! 大石泉すき"'
ここでついにperlではなくシェルのコマンドになりました。
実は-eの直後の空白も削れます。
$ perl -e'use v5.10;$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;say"$idol到達! 大石泉すき"'
-E大作戦
$ perl -E'$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;say"$idol到達! 大石泉すき"'
-Eは-eと同様にコマンド引数にスクリプトを与えるオプションですが、use v5.10;を書かなくてもsayを使えるようにする効果があります。
ボツ案
mapの代わりにforを使う大作戦
$ perl -E'$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;say"$idol到達! 大石泉すき"'
$ perl -E'$izumi=1x4;do{/00$/&&say,$_++while$_-$izumi}for$idol;say"$idol到達! 大石泉すき"'
map { 本文 } 配列式は、ただのループ構文として使うなら本文 for 配列の方が{}の分だけ節約できます。しかしforの左辺は式でなければならず、do{}式を置いたために長くなってしまいました。中身のwhileがなければいいのですが、条件が真の間ループする方法が思いつかずボツ。
do { 本文 }は、本文を評価した結果を返す式です。本文には文を書くことができます。
mapの{}を省略する大作戦
$ perl -E'$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;say"$idol到達! 大石泉すき"'
$ perl -E'$izumi=1x4;map do{/00$/&&say,$_++while$_-$izumi},$idol;say"$idol到達! 大石泉すき"'
map { 本文 } 配列式は、本文が特定の条件に当てはまる式のとき、map 本文, 配列と書けます。しかし今回本文はwhile文なので結局do式で囲まなければならず、文字数が増えてしまいました。
エイリアス大作戦
$ perl -E'$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;say"$idol到達! 大石泉すき"'
$ perl -E'$izumi=1x4;*_=\$idol;/00$/&&say,$_++while$_-$izumi;say"$idol到達! 大石泉すき"'
ここではmapを「$_で$idolを参照できるようにする」用途で使っていますが、別に同じことは*_ = \$idol;でもできます。しかしforやmapは暗黙に$_に入れてくれるけどこれでは指定しないといけないので結局文字数が変わらなくてボツ。
インクリメント埋め込み大作戦
$ perl -E'$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;say"$idol到達! 大石泉すき"'
$ perl -E'$izumi=1x4;map{/00$/&&say while++$_-$izumi}$idol;say"$idol到達! 大石泉すき"'
インクリメントを条件式に埋め込むと少し文字数が変わりますが、処理順の変更によって、レギュレーションに違反する動作を行うようになったのでボツ。
インクリメントの順序を変えると、izumiが1111のときにたまたまよくできていても、0や1200のときに意図しない動作になります。
redo大作戦
$ perl -E'$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;say"$idol到達! 大石泉すき"'
$ perl -E'$izumi=1x4;$_-$izumi&&(/00$/&&say,$_++,redo)for$idol;say"$idol到達! 大石泉すき"'
forとwhileでループ構文を2度使うなら、forの中でredoすれば無限ループになるじゃないかという大作戦。redoは、それが書かれているループの本文を最初から同じ条件でやり直します。
$_-$izumiが真の場合、/00$/&&sayで$idolが0以外の100の倍数の時の表示処理を行って、$_++でインクリメントして、redoでループを続けます。偽の場合はredoをしないのでループが終わります。実質whileと同じです。
しかしなんだかんだで文字数が減らせなかったのでボツ。
-p大作戦
$ perl -E'$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;say"$idol到達! 大石泉すき"'
$ echo|perl -pE'$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;$_="$idol到達! 大石泉すき\n"'
$ id|perl -pE'$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;$_="$idol到達! 大石泉すき\n"'
$ id|perl -pE'$izumi=1x4;*_=\$idol;/00$/&&say,$_++while$_-$izumi;$_.="到達! 大石泉すき\n"'
$ id|perl -lpE'$izumi=1x4;*_=\$idol;/00$/&&say,$_++while$_-$izumi;$_.="到達! 大石泉すき"'
$ id|perl -lpE'$izumi=1x4;*_=\$idol;/00$/&&say,$_++while$_-$izumi;s/$/到達! 大石泉すき/'
-pオプションで自動的に出力させたらsayしなくてもいいのではないかという大作戦。
-pはスクリプトをwhile (<>) { 本文 } continue { print; }で囲って実行するらしいです。暗黙にprintが追加されるのでスクリプト中でsayしなくてもいいのですが、while (<>)が問題で、標準入力を1行分だけ与えなければなりません。それにはechoを使うことができますが、4文字はあまりにも長すぎます。
$ echo|perl -pE'$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;$_="$idol到達! 大石泉すき\n"'
echoに変わるもっと短いコマンドはないでしょうか。Bashで1文字で使えそうなコマンドは. [ : wあたりですが、どれも該当しませんでした。2文字ならユーザー情報を表示するidコマンドが確定で1行だけ出力してくれるようです。コマンドの頭にid|を付けることで3文字の消費でwhile (<>)部分を打ち消すことが出来ました。
$ id|perl -pE'$izumi=1x4;map{/00$/&&say,$_++while$_-$izumi}$idol;$_="$idol到達! 大石泉すき\n"'
最後に表示するのは$_ですが、エイリアス大作戦を使えば$_が$idolを表したままにできるので、$_に到達! 大石泉すきを結合するだけでよくなります。(いくら$idolと$_が同じ実体を表すとは言え、$_を出力するのはレギュレーション違反な気もしますが)
$ id|perl -pE'$izumi=1x4;*_=\$idol;/00$/&&say,$_++while$_-$izumi;$_.="到達! 大石泉すき\n"'
まだ削れます。-lオプションを付けると末尾に改行を自動的に付けてくれるので、\nを削れます。
$ id|perl -lpE'$izumi=1x4;*_=\$idol;/00$/&&say,$_++while$_-$izumi;$_.="到達! 大石泉すき"'
まだ削れます。$_の末尾への追加は、s/$/文字列/という置換処理で代用できます。
$ id|perl -lpE'$izumi=1x4;*_=\$idol;/00$/&&say,$_++while$_-$izumi;s/$/到達! 大石泉すき/'
そして、結局文字数は変わりませんでした。ボツ。