koluku's blog

コードは毎日の欠片

Perlでズンドコ問題を短くしてみる

こちらはPerl Advent Calendar 2020の20日目の記事になります。

昨日はtecklさんのPerlでGitHub webhookを受けるbotを作ってみた話でした。

qiita.com

先週何も考えずにAdvent Calendarに登録して今日が投稿日だということをメールで知りました。一文字たりとも何も書いてないぞ、過去の僕恨む。


FizzBuzz記事の振り返り

Perlネタといえば、先々週にこんな記事を書いてました。

techblog.kayac.com

FizzBuzz問題を短く書くにはどういう思考過程をすればいいのかという初級者向けな感じの内容です。この記事を書いている間にちょっとブコメを見てみたらもっと短縮されてました。

うたがわさんのコードは、割り切れた結果が0で返ってくることを利用して!で反転させて繰り返し演算子で出力しています。

id:utgwkk おもしろい / もうちょっと短くできた $ perl -E 'say"Fizz"x!($%3)."Buzz"x!($%5)||$_ for 1..100'

すぎゃーんさんのコードは、(Fizz)でリストに入れて[$_%3]でリスト内のインデックスを参照して該当する要素があれば出力しています。

id:sugyan perl -E'say((Fizz)[$%3].(Buzz)[$%5]||$_)for 1..1e2'

(Fizz) で Bareword が文字列扱いになって("Fizz")のリストになるのも言われてみればという感じで、他言語から入った人なのでリストの要素数を超えて取ろうとしてもエラーにならずにundefになるのが意外でした。(PythonやGoの経験から「いや、Out of rangeになるだろ」という先入観がありました)

ズンドコ問題で短くしてみる

さきほどの記事の最後に「じゃあズンドコ問題でもやってみよう」などと書いたわけなのでやってみます。

ズンドコ問題はFizzBuzz問題の逆で、明確な回数でループが終了しません。また、ズンドコの判定も配列や文字列を使って自由にできそうなのもポイントです。

さてこの問題、Slackのログを見てみるとPerlを学びたての頃はこんなコードを書いていました。

use 5.024;
use strict;
use warnings;

my ($z, $d, $k) = ('ズン', 'ドコ', 'キ・ヨ・シ!');
my @choice = ($z, $d);
my $match = join('', my @tmp = ($z, $z, $z, $z, $d));
my @array = ();
while(join('', @array) ne $match) {
    say my $last = @choice[int(rand($#choice+1))];
    push @array, $last;
    shift @array if $#array == 5;
}
say $k;

内容としては「配列に5個ピッタシになるようにキューで入れて確認する」という仕組みですが、色々と無駄がありそうです。まずはこのコードを改善してみます。

まず、このままでは読みづらいのでバージョン宣言以外を抜くのと一般的なコードに戻す作業をします。

use 5.024;
my ($z, $d) = qw(ズン ドコ);
my @choice = ($z, $d);
my $match = join '', ($z, $z, $z, $z, $d);
my @array;
while(join('', @array) ne $match) {
    say my $last = @choice[int(rand(scalar @choice))];
    push @array, $last;
    shift @array if scalar @array == 6;
}
say 'キ・ヨ・シ!';

大きく変更した点は以下の3つです。

  • qw演算子はスペース区切りの文字列を分割して文字列のリストとして返す
  • 配列は宣言した段階でも空配列として扱われる
  • 配列はスカラーコンテキストのときに配列の要素数を返す

このコードではズンかドコのどちらかを配列に入れた後に文字列に戻して確認しています。それなら最初から文字列で入れたほうが効率的です。

use 5.024;
my ($z, $d) = qw(ズン ドコ);
my @choice = ($z, $d);
my $result;
while($result !~ /$z$z$z$z$d$/) {
    say my $tmp = @choice[int(rand(scalar @choice))];
    $result .= $tmp;
}
say 'キ・ヨ・シ!';

ループするごとに文字列を結合していき、最後の5文字がズンズンズンズンドコかどうかを正規表現で確認しています。

今度はズンかドコのどちらかを出力するコードが無駄が多く見えるので出力を改善してみます。途中出力はズンかドコの2択なので三項演算子でできそうです。

use 5.024;
my ($z, $d) = qw(ズン ドコ);
my $r;
while($r !~ /$z$z$z$z$d$/) {
    say my $t = rand > .5 ? $z : $d;
    $r .= $t;
}
say 'キ・ヨ・シ!';

かなりスッキリしました。

rand関数は指定しなかった場合には引数を1として扱い、0以上1未満の範囲で返すため0.5で分岐になります。

perldoc.jp

ワンライナーにコードを省略すると85文字になりました。良さそうですね。

$ perl -E '($z,$d)=qw(ズン ドコ);$r;while($r!~/$z$z$z$z$d$/){say$t=rand>.5?$z:$d;$r.=$t;}say"キ・ヨ・シ!"'

ところで、ズンとドコを変数に代入しましたが、文字列を代入するだけならqw演算子で宣言するよりも直接代入したほうが短い事があります。

$z="ズン";$z="ドコ"; # 2文字短い

また、$z$dとは同じ2文字なので文字数には差がありません。変数を使わないことでも短くなりそうです。

perl -E '$r;while($r!~/ズンズンズンズンドコ$/){say$t=rand>.5?"ズン":"ドコ";$r.=$t;}say"キ・ヨ・シ!"'

さらに、正規表現を使っているのでグループ化して省略もできそうです。最後の$ も、マッチが見つかった時点で終了するので不要ですね。

これで68文字まで短くなりました。

perl -E '$r;while($r!~/(ズン){4}ドコ/){say$t=rand>.5?"ズン":"ドコ";$r.=$t}say"キ・ヨ・シ!"'

ちょっとずるいですが、ズンとドコを改行なしで出力することで短くすることができます。

Perlでいかにして短いコードを書けるかを考えてみる - KAYAC engineers' blogでは後置forを使いましたが、ここでもその発想は活きそうです。内容としては後置whileでズンドコの条件に当てはまるまで文字列結合を行います。

use 5.024;
my $r;
$r .= rand > .5 ? "ズン" : "ドコ" while $r!~/(ズン){4}ドコ/;
say $h, "キ・ヨ・シ!";

これをワンライナーに戻すと56文字まで短くなりました!

perl -E '$h.=rand>.5?"ズン":"ドコ"while$h!~/(ズン){4}ドコ/;say$h,"キ・ヨ・シ!"'

終わりに

コードを書くための思考過程を追う記事ってあんまりなさそうなので書いてみましたが、普段自分の頭の中でぼんやりと考えていることを言語化できて面白いので機会があれば書きたいなと思いました。

明日のアドベントカレンダーは誰が書くのでしょう、お楽しみに!

Perl Advent Calendar 2020 - Qiita