更新履歴

  1. : 公開
  2. : 小さな文言の修正・変更

記事の構成について

この記事は、普通の fizzbuzz を徐々に変形して最終形にしていく、という構成で書かれている。最終形を見てどのような仕組みで動いているのか解読してから解説を読みたい、というかたがいれば、このページにソースコードがあるので、そちらを先に見てほしい。

レギュレーション

PHP で、次のような制約の下に fizzbuzz を書いた。

  • 1行あたりの文字数は2文字までに収めること (ただし<?phpタグは除く)

    • 厳密な定義:<?phpタグ以降のソースコードが、2 byte ごとに ラインフィード (LF) で区切られること

  • スペースやタブを使用しないこと

  • ループのアンロールをしないこと

    • 100 回ループの代わりに 100 回コードをコピペ、というのは禁止

  • PHP 7.4〜8.1 で動作すること

  • 実行時に Notice や Warning が出ないこと

  • 標準的なインストール構成の PHP で実現できること (デフォルトで有効になっていない拡張等を使わないこと)

備考: PHP にはshort_open_tagというオプションがあり、これを有効にするとファイル冒頭の<?phpの代わりに<?を使うことができ、文字どおり1行2文字で書ける。ただ、このオプションはデフォルト off になっている環境が多いようなので、今回は使わないことにした。

主な障害

1行あたりの文字数など、適当に改行を挟めばいいだけではないのか?

特に、C言語でこのような試みをおこなったことがあるかたならそう思うだろう。事実、Cでのこの制約はほとんど無意味に等しい。

              #\
  i\
  n\
  c\
  l\
  u\
  d\
  e\
  <\
  s\
  t\
  d\
  i\
  o\
  .\
  h\
  >\
  /*
  */
  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 では文字列リテラル中に生の改行が書けるので

              $a
='
a'
;;
            

とすると$a"\na"になるのだが、余計な改行が入ってしまう。

これらの障害をどのように乗り越えるのか、次節から見ていく。

解説

普通の (?) fizzbuzz

まずは普通に書くとしよう。

                <?php

    for ($i = 1; $i < 100; $i++) {
    echo (($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n";
    }
              

素直に書いた fizzbuzz とは言い難いが、このくらいは普通だということにしておかないと、この先がやっていられないので許してほしい。

forの排除

forは、3文字もある長いキーワードである。こんなものは使えない。array_系の関数を使って、適当に置き換えるとしよう。

                <?php

  $s = range(1, 100);
  array_walk(
  $s,
  fn($i) =>
  printf((($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"),
  );
              

array_walkrangeprintfといったforよりも長いトークンが現れてしまったが、これは次節で直すことにする。なお、echoは文 (statement) であり式 (expression) ではないので、式であるprintfに置き換えた。

関数呼び出しの短縮

rangearray_walkprintfは長すぎるのでどうにかせねばならない。ここで、PHP の可変関数を使う。可変関数とは、関数名が文字列として入った変数を経由して、関数を呼び出す機能である。

                <?php

  $r = 'range';
  $w = 'array_walk';
  $p = 'printf';

  $s = $r(1, 100);
  $w(
  $s,
  fn($i) =>
  $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という文字列が欲しければ、次のようにする。

                $f
=F
.i
.z
.z
;;
              

こうして簡単に文字列を作れる。なお、この仕様は 7.x 時点でも警告を受けるので、@演算子を使って抑制してやるとよい。

                $f
=@
F.
@i
.#
@z
.#
@z
;;
              

むしろ、このことがわかっていたからこそ PHP 8.x での動作を要件に課したところがある。

文字列リテラルの短縮

実際に使った手法の説明に移る。

ずばり、文字列同士のビット演算を使う。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
              

これを踏まえ、次のコードを見てみよう。

                $x = "x\nOm\n";
$y = "\nk!\no";
$r = $x ^ $y;
echo "$r\n";
              

実行すると、rangeが表示される。さて、PHP では文字列リテラル中に生の改行を直接書いてもよいのだった (「主な障害」の節を参照のこと)。書きかえてみよう。

                $x
='x
Om
';
$y
='
k!
o'
;

$r = $x ^ $y;
echo "$r\n";
              

さらに#を使って適当に調整すると、次のようになる。

                $x
=#
'x
Om
';
$y
='
k!
o'
;#
$r
=#
$x
^#
$y
;#

echo "$r\n";
              

1行あたり2文字で、rangeという文字列を生成することに成功した。他の必要な文字列にも、同様の処理をほどこす。

備考:Buzz中にある小文字のuは、このロジックだと non-printable な文字になってしまう。ここまでのテクニックを駆使すれば回避するのはそう難しくないので、考えてみてほしい。

完成系

完成したものがこちら。

              <?php

  $x
  =#
  'i
  S'
  ;;
  $y
  ='
  b!
  ';
  $c
  =#
  $x
  ^#
  $y
  ;#
  $x
  =#
  'x
  Om
  ';
  $y
  ='
  k!
  o'
  ;#
  $r
  =#
  $x
  ^#
  $y
  ;#
  $x
  =#
  'k
  Sk
  ~}
  Ma
  ';
  $y
  ='
  x!
  s!
  k!
  ';
  $w
  =#
  $x
  ^#
  $y
  ;#
  $x
  =#
  'z
  Hd
  G'
  ;#
  $y
  ='
  x!
  ~!
  ';
  $p
  =#
  $x
  ^#
  $y
  ;#
  $x
  =#
  'L
  [p
  ';
  $y
  ='
  c!
  ';
  $f
  =#
  $x
  ^#
  $y
  ;#
  $x
  =#
  'H
  [p
  ';
  $y
  ='
  _!
  ';
  $b
  =#
  $x
  ^#
  $y
  ;#
  $b
  [1
  ]=
  $c
  (#
  13
  *9
  );
  $s
  =#
  $r
  (1
  ,(
  10
  **
  2)
  );
  $w
  (#
  $s
  ,#
  fn
  (#
  $i
  )#
  =>
  $p
  ((
  (#
  $i
  %3
  ?#
  ''
  :#
  $f
  ).
  (#
  $i
  %5
  ?#
  ''
  :#
  $b
  )?
  :#
  $i
  )#
  .'
  ')
  );
            

感想など

PHP は、スクリプト言語の中だとシンタックスシュガーが少ない (体感)。この挑戦は不可能に思われたが、PHP マニュアルとにらめっこしていたらなんとかなった。

みんなもプログラムを細長くしよう。

余談2: 別解

PHP では、バッククォートを使ってシェルを呼び出せる。これはshell_exec関数と等価である。さて、PHP ではバックスラッシュによる行継続が使えないと書いたが、シェルでは使える (当然だが、呼び出されるシェルに依存する。Bash なら大丈夫だろう。知らんけど)。

              <?php

  printf(`
  e\
  c\
  h\
  o\
  \
  1\
  2\
  3\
  `);
            

なお、ここでは簡単のため出力にprintfをそのまま使っているが、実際にはprintfという文字列を合成して可変関数で呼び出す。

ただし、これでは

  • スペースやタブを使用しないこと

に違反してしまう。スペースが使えないと引数とコマンドを区切れない。これは困った。

もうこれ以上は不可能だと思っていたのだが、この記事の執筆中に解決する方法を思いついたので載せておく。

              <?php

$c = 'chr';

${
'_
'}
=#
$c
(#
32
).
$c
(#
92
);

printf(`
e\
c\
h\
o\
${
'_
'}
1\
2\
3\
`);
            

先程と同じく、chrprintfを生成する部分は長くなるので省いた。

              ${
'_
'}
            

は変数で、中にはスペースとエスケープが入っている (chr(32) . chr(92))。シェルに渡されている文字列は次のようになる。

              e\
c\
h\
o\
\
1\
2\
3\
            

これは、前掲したコマンドと同じだ。かくして、スペースを陽に書かずにシェルをおおよそ自由に扱えるようになった。Fizzbuzz のワンライナーくらいすぐ書けるだろうから、あとはなんとかなるだろう (試してないけど)。

ということでこれは別解ということにしておく。

ちなみに、PHP 8.2 からは、この記法で Warning が出るようになるようだ。

              ${
'_
'}
            

最新版で警告が出るというのも美しくないので、私としては本編の解法を推す。