FizzBuzzから始めるコードの再利用性を高めるトレーニング

  • 33
    いいね
  • 0
    コメント

プログラムを書けるプログラマを採用することは難しい、と説明した「どうしてプログラマに・・・プログラムが書けないのか?」といふ有名なエッセイがある。このエッセイによって「プログラムを書けるプログラマ」を見分けるためのテストとして良く知られるようになった「FizzBuzz問題」がある。

1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

(どうしてプログラマに・・・プログラムが書けないのか?: Jeff Atwood / 青木靖 訳 / 2007年2月26日より引用)

もちろんこれは、最低限の能力があるかどうかを図るための質問に過ぎない。いまどき採用面接でFizzBuzzを解かせる会社があるのかは知らないけれど、せめて自信を持って解けるようになっておきたいところ。

この記事では、簡単な問題から徐々に要件を増やしていって、目的意識を持ってプログラムを書けるようになることを目標としたい。この記事のコードはつっこみどころがあったり、わかりやすさを優先してベストな書き型とはちょっとずらして書いてあるところがある。つまりはベストプラクティスとして参考にすべきではない(俺の方がもっと綺麗に書けるぞ、と感じていただけるとありがたい)

PHPで説明するが、ほかのプログラミング言語でも大差はないだろう。

この記事の読みかた

  1. 上の条件に従って、(自分の得意な言語で)FizzBuzz問題を解いてみてください
  2. それぞれの条件を満たすように変更し、参考コードを見比べてみてください

自分の得意な言語では楽勝だよ、ってひとは触ったことがない言語で解いてみるといいですね。

[質問1] 1から100までの数をプリントするプログラムを書けるか?

<?php

// forを使った方法
for ($i = 1; $i <= 100; $i++) {
    echo $i, "\n";
}

// 別解
foreach (range(1, 100) as $i) {
    echo $i, "\n";
}

解説

これに関しては特に説明は要らないだろう。どちらの解でも問題はないが、「PHP経験あります」と言ってこの構文を説明できなければ、試験官はPHP経験者としてあなたに接することができなくなるだろう。

ここでありがちなケアレスミスとしては、開始条件と終了条件だろう。設問には「1から」とあるが、最初の表示が0だったり、最後の表示が99だったり101だったり、といったところだ。試験官はこのミスで深刻な減点はしないかもしれないが、あなたを注意力の浅い人間だと疑ってかかるかもしれない。

[質問2] 3の倍数なら「Fizz」 5の倍数なら「Buzz」 両方の倍数なら「FizzBuzz」

<?php

for ($i = 1; $i <= 100; $i++) {
    if ($i % 3 == 0) {
        echo 'Fizz';
    }
    if ($i % 5 == 0) {
        echo 'Buzz';
    }
    if (!($i % 3 == 0) && !($i % 5 == 0)) {
        echo $i;
    }

    echo "\n";
}

// 別解多数

解説

この処理の書きかたについては非常に多くのパターンがある。要点は「倍数の判定ができるか」「(小学校で習った)あまりのある割り算ができるか」といったところだろう。

[質問3] FizzBuzzを関数に分割できるか?

<?php

for ($i = 1; $i <= 100; $i++) {
    echo toFizzBuzz($i), "\n";
}

/**
 * 数字を「FizzBuzz化」する
 *
 * @param  int    $n
 * @return string
 */
function toFizzBuzz($n)
{
    $s = '';
    if ($n % 3 == 0) {
        $s .= 'Fizz';
    }
    if ($n % 5 == 0) {
        $s = 'Buzz';
    }
    if ($s === '') {
        $s = "{$n}";
    }

    return $s;
}

// 別解多数

解説

ここでは「数字を受け取ってFizzBuzz的な文字列で表現する」関数をtoFizzBuzz()と名付けた。

[質問4] FizzBuzzのユニットテストを書けるか?

<?php

assert(toFizzBuzz(1) === '1');
assert(toFizzBuzz(3) === 'Fizz');
assert(toFizzBuzz(5) === 'Buzz');
assert(toFizzBuzz(15) === 'FizzBuzz');
assert(toFizzBuzz(45) === 'FizzBuzz');
assert(toFizzBuzz(99) === 'Fizz');
assert(toFizzBuzz(100) === 'Buzz');
assert(toFizzBuzz(101) === '101');

for ($i = 1; $i <= 100; $i++) {
    echo toFizzBuzz($i), "\n";
}

function toFizzBuzz($n)
{
    $s = '';
    if ($n % 3 == 0) {
        $s .= 'Fizz';
    }
    if ($n % 5 == 0) {
        $s = 'Buzz';
    }
    if ($s === '') {
        $s = "{$n}";
    }

    return $s;
}

もしかするとPHPUnitphpspecのようなテスティングフレームワークがないとテストができないと想像するかもしれないが、ここではPHPのassert()機能を使った。Pythonでもassert toFizzBuzz(5) == "Buzz"のように書けるし、Rubyならばraise unless toFizzBuzz(5) == "Buzz"のような文で簡易テストできるだろう。

テストのためにどんな値を選んで検査するか、このテストは適切か、もっと書きやすい方法はないか、といったことには一考の余地がある。同値分割境界値分析といったことばは頭の片隅に置いておいた方が良いかもしれない。

ところで、ここで書いたコードにはこれ見よがしなバグが存在する。かんたんな実装であってもTDD(テスト駆動開発)を実践することでバグが混入する可能性をいくらか減らすことができるかもしれない。

[質問5] HTML出力とテキスト出力を簡単にスイッチできるようにできるか?

<?php

$html_printer = function ($elements) {
    $s = "<ol>\n";
    foreach ($elements as $element) {
        $s .= sprintf("<li>%s</li>\n", htmlspecialchars($element, ENT_QUOTES));
    }
    $s .= "</ol>\n";

    return $s;
};

$text_printer = function ($elements) {
    $s = "";
    foreach ($elements as $element) {
        $s .= "{$element}\n";
    }

    return $s;
};

$printer = (isset($argv[1]) && ($argv[1] === 'html')) ? $html_printer : $text_printer;

$values = [];
for ($i = 1; $i <= 100; $i++) {
    $values[] = toFizzBuzz($i);
}
echo $printer($values);

解説

Template Methodパターンの、すっごくシンプルにしたバージョン。

[発展質問] 上記のパターンをメモリ使用量を削減できるか?

PHP中級者以上向け。

<?php

// これは飽くまで解法のひとつ、ほかの方法もある

$html_printer = function ($elements, $out) {
    fwrite($out, "<ol>\n");
    foreach ($elements as $element) {
        fwrite($out, "<li>{$element}</li>\n");
    }
    fwrite($out, "</ol>\n");
};

$text_printer = function ($elements, $out) {
    foreach ($elements as $element) {
        fwrite($out, "{$element}\n");
    }
};

$printer = (isset($argv[1]) && ($argv[1] === 'html')) ? $html_printer : $text_printer;

$printer(fizzbuzz(1, 100), fopen('php://stdout', 'w'));


function fizzbuzz($begin = 1, $end = 100)
{
    for ($i = $begin; $i <= $end; $i++) {
        yield toFizzBuzz($i);
    }
}

// メモリ使用量チェック
var_dump(memory_get_peak_usage());

解説

私の環境でためしに$end = 1000000にしてみたところ、[質問5]のバージョンで72737088(72MB)、今回のバージョンで407016(407KB)だったので効果はありそうですね。今回はジェネレータを使ってみました。

PHPではechofwrite(STDOUT, $str)fwrite(fopen('php://stdout', 'w'), $str)file_put_contents('php://stdout', $str)の意味は(だいたい)いっしょです。

なぜ[質問5]のバージョンでメモリを消費し、ここまで差がつくのか説明できると良いですね。

あとがき

原稿とPHPカンファレンスの進捗だめなので、これからがんばります。

おまけ