= [PHP] fizzbuzz を書く。1行あたり2文字で。 :tags: php :description: PHP で、fizzbuzz を書いた。ただし、1行あたりに使える文字数は2文字まで。 :revision-1: 2022-09-28 公開 :revision-2: 2022-09-29 小さな文言の修正・変更 == 記事の構成について この記事は、普通の fizzbuzz を徐々に変形して最終形にしていく、という構成で書かれている。最終形を見てどのような仕組みで動いているのか解読してから解説を読みたい、というかたがいれば、 https://gist.github.com/nsfisis/04c227d5a419867472a0b23a83ad2919#file-fizzbuzz-php-2-letters-per-line-and-supports-php-8-x-without-warnings[このページ] にソースコードがあるので、そちらを先に見てほしい。 == レギュレーション PHP で、次のような制約の下に fizzbuzz を書いた。 * 1行あたりの文字数は2文字までに収めること (ただし `\ /* */ i\ n\ t\ /* */ m\ a\ i\ n( ){ f\ o\ r( i\ n\ t\ /* */ i= 1; i< 1\ 0\ 0; i\ +\ +) if (i %\ 15 == 0) p\ r\ i\ n\ t\ f( "\ F\ i\ z\ z\ B\ u\ z\ z\ %\ c\ ", 10 ); /* あとは同じように普通のプログラムを変形するだけなので省略 */ ---- バックスラッシュを使った行継続がトークンを区切らない、というのがポイントだ。 さて、PHP ではそもそもバックスラッシュを行継続に使うことができない。これにより、「3文字以上からなるトークンが一切使えない」という制約が課される。例えば、`echo` で出力することや、`for` でループすること、`new` でインスタンスを生成することができない。特に、出力は fizzbuzz をどんなアルゴリズムで実装しようとおこなわなければならないので、できないのは致命的である。 当然、名前が3文字以上ある関数も使えない。なお、標準 PHP の範囲内において、名前が 2文字以下の関数は以下のとおりである: * `_`: `gettext` のエイリアス * `dl`: 拡張モジュールをロードする * `pi`: 円周率を返す (環境によって多少は変わるかも) 2文字の関数を定義しまくった拡張モジュールを用意しておいて `dl()` で読み込む行為は、レギュレーションで定めた ____ * 標準的なインストール構成の PHP で実現できること (デフォルトで有効になっていない拡張等を使わないこと) ____ に反する (というより、「それだとおもしろくもなんともないので、このルールを足した」というのが正しい)。 また、2文字だと文字列がまともに書けないのも辛い。`''` だけで 2文字使うので、「1文字の文字列リテラル」というものを書くことができない。PHP では文字列リテラル中に生の改行が書けるので [source,php] ---- $a =' a' ;; ---- とすると `$a` は `"\na"` になるのだが、余計な改行が入ってしまう。 これらの障害をどのように乗り越えるのか、次節から見ていく。 == 解説 === 普通の (?) fizzbuzz まずは普通に書くとしよう。 .... printf((($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"), ); ---- `array_walk` や `range`、`printf` といった `for` よりも長いトークンが現れてしまったが、これは次節で直すことにする。なお、`echo` は文 (statement) であり式 (expression) ではないので、式である `printf` に置き換えた。 === 関数呼び出しの短縮 `range`、`array_walk`、`printf` は長すぎるのでどうにかせねばならない。ここで、PHP の可変関数を使う。可変関数とは、関数名が文字列として入った変数を経由して、関数を呼び出す機能である。 [source,php] ---- $p((($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"), ); ---- これで関数を呼び出している所は短くなった。では、`$r` や `$w` や `$p`、また `'Fizz'` や `'Buzz'` はどうやって 1行2文字に収めるのか。次のテクニックへ移ろう。 === 余談: PHP 8.x で動作しなくてもいいなら 今回使ったテクニックを説明する前に、余談として、文字列リテラルの短縮法として今回採用しなかったものを紹介する。 ____ * PHP 7.4〜8.1 で動作すること ____ というルールがない場合、「未定義の定数が評価された場合、その定数の名前が値になる」という PHP 7.x までの仕様が利用できる。例えば、 `Fizz` という文字列が欲しければ、次のようにする。 [source,php] ---- $f =F .i .z .z ;; ---- こうして簡単に文字列を作れる。なお、この仕様は 7.x 時点でも警告を受けるので、`@` 演算子を使って抑制してやるとよい。 [source,php] ---- $f =@ F. @i .# @z .# @z ;; ---- むしろ、このことがわかっていたからこそ PHP 8.x での動作を要件に課したところがある。 === 文字列リテラルの短縮 実際に使った手法の説明に移る。 ずばり、文字列同士のビット演算を使う。PHP では、文字列同士でビット演算 (`&`、`|`、`^`) をした場合、文字列の各バイトごとに指定したビット演算がなされ、それを結合したものが演算結果となる。 [source,php] ---- $a = "12345"; $b = "world"; // $a ^ $b は次のコードと同じ $result = ''; for ($i = 0; $i < min(strlen($a), strlen($b)); $i++) { $result .= $a[$i] ^ $b[$i]; } echo $result; // => F]AXQ ---- これを踏まえ、次のコードを見てみよう。 [source,php] ---- $x = "x\nOm\n"; $y = "\nk!\no"; $r = $x ^ $y; echo "$r\n"; ---- 実行すると、`range` が表示される。さて、PHP では文字列リテラル中に生の改行を直接書いてもよいのだった (「主な障害」の節を参照のこと)。書きかえてみよう。 [source,php] ---- $x ='x Om '; $y =' k! o' ; $r = $x ^ $y; echo "$r\n"; ---- さらに `#` を使って適当に調整すると、次のようになる。 [source,php] ---- $x =# 'x Om '; $y =' k! o' ;# $r =# $x ^# $y ;# echo "$r\n"; ---- 1行あたり2文字で、`range` という文字列を生成することに成功した。他の必要な文字列にも、同様の処理をほどこす。 備考: `Buzz` 中にある小文字の `u` は、このロジックだと non-printable な文字になってしまう。ここまでのテクニックを駆使すれば回避するのはそう難しくないので、考えてみてほしい。 == 完成系 完成したものがこちら。 [source,php] ---- $p (( (# $i %3 ?# '' :# $f ). (# $i %5 ?# '' :# $b )? :# $i )# .' ') ); ---- == 感想など PHP は、スクリプト言語の中だとシンタックスシュガーが少ない (体感)。この挑戦は不可能に思われたが、PHP マニュアルとにらめっこしていたらなんとかなった。 みんなもプログラムを細長くしよう。 == 余談2: 別解 PHP では、バッククォートを使ってシェルを呼び出せる。これは `shell_exec` 関数と等価である。さて、PHP ではバックスラッシュによる行継続が使えないと書いたが、シェルでは使える (当然だが、呼び出されるシェルに依存する。Bash なら大丈夫だろう。知らんけど)。 [source,php] ----