読者です 読者をやめる 読者になる 読者になる

テスト書ける系fizzbuzz 〜generatorを添えて〜

みなさん、こんにちは。

今日は新春ということで、fizzbuzzについてお話します。

世の中には大きくわけて2つのfizzbuzzがあります。

一つ目はテスト書ける系fizzbuzzです。

もう一つはテスト書けない系fizzbuzzです。

今日はみなさんに併せて3種類のfizzbuzzの実装をお見せして、

テスト書ける系fizzbuzzにgeneratorを用いると、どのような利点が生まれるかご紹介します。

ちなみに研修でロジカルシンキングおじさんに生まれ変わったので、話し方がロジカルになりました。

ごちゃまぜfizzbuzz

まずテスト書けない系fizzbuzz筆頭の普通のfizzbuzz

<?php
function fizzbuzz($n)
{
    foreach (range(1, $n) as $i) {
        if ($i % 15 == 0) {
            print("FizzBuzz\n");
        } elseif ($i % 5 == 0) {
            print("Buzz\n");
        } elseif ($i % 3 == 0) {
            print("Fizz\n");
        } else {
            print("${i}\n");
        }
    }
}

fizzbuzz(100);

これでも動きますが、これではテストが書けません。

文字列fizzbuzz

上記の反省を活かしてテスト書ける系fizzbuzzを書いてみましょう。

まず思いつくのは"1\n2\nFizz\n"...といった文字列を返却するstring_fizzbuzzメソッドを呼び出し、それを表示する方法です。*1

<?php
function string_fizzbuzz($n)
{
    $fizzbuzz = "";
    foreach (range(1, $n) as $i) {
        if ($i % 15 == 0) {
            $fizzbuzz .= "FizzBuzz\n";
        } elseif ($i % 3 == 0) {
            $fizzbuzz .= "Fizz\n";
        } elseif ($i % 5 == 0) {
            $fizzbuzz .= "Buzz\n";
        } else {
            $fizzbuzz .= "${i}\n";
        }
    }
    return $fizzbuzz;
}

function fizzbuzz($n)
{
    print(string_fizzbuzz($n));
}

fizzbuzz(100);

これでOKです。 string_fizzbuzzで返された文字列を検査することで、printの出力先や、print自体のバグに悩まされること無く、fizzbuzzのロジック部分をテストすることができます。

ジェネレータfizzbuzz

string_fizzbuzzでは、ロジック部分と出力部分を分離することによってテストが可能になりました。 この実装でもいいのですが、 fizzbuzzの最終値がとても大きい場合や、各$iにおいてfizzbuzz内で返す値を決めるのに時間がかかる場合に、ちょっと面倒なことになります。

例として、下記のような処理を考えます。

<?php
function string_hoge($n)
{
    $acc = "";
    foreach (range(1, $n) as $i) {
      // 重い処理で次の値を決定
      $value = omoi_syori();

      $acc.= $value;
    }
    return $acc;
}

function hoge($n)
{
    print(stringe_hoge($n));
}

// すごく大きな値
fizzbuzz(9999....);

このような場合、実際に値が表示されるのは、重い処理が9999....回終わったあとになります。 最初の方の結果を見るのに長い時間待たされる上に、途中でエラーが落ちてシステムがクラッシュしたら途中までの値を見ることもできません。

また、9999.....個分という巨大なfizzbuzz文字列を格納するためのメモリ消費も、本質的には必要のないコストです。

このようなつらい事態を回避するために、generatorを使うことが出来ます。

コードは下記のようになります。

<?php
function generator_fizzbuzz()
{
    $fizzbuzz_logic = function ($n) {
        if ($n % 15 == 0) {
            return "FizzBuzz";
        } elseif ($n % 5 == 0) {
            return "Buzz";
        } elseif ($n % 3 == 0) {
            return "Fizz";
        } else {
            return $n;
        }
    };

    for ($i = 1;; ++$i) {
        yield $fizzbuzz_logic($i);
    }
}

function fizzbuzz($n)
{
    $g = generator_fizzbuzz();
    foreach (range(1, $n) as $i) {
        print($g->current() . "\n");
        $g->next();
    }
}

fizzbuzz(100);

このコードでは、generator_fizzbuzz呼び出し時にyieldまで実行が進み、yieldで指定された値が返り値として返却される→返却された値がfizzbuzz関数内でprintされる。という処理が行われます。

printした後の処理ですが、fizzbuzz内で$g->next()されると、先ほど中断したところ(yieldの次の行から)から再度実行が始まります。

このようにfizzbuzzの実装にgeneratorを用いることで、テストが書ける構造は維持した上で

  • fizzbuzzを最後まで処理しなくても値を1つずつ表示できるため待たなくて良い。
  • 巨大な文字列をメモリに格納しなくてもよい。

といった時間やメモリに関する面での性能を改善することができます。

まとめ

テスト書けない系fizzbuzzを見てもまさかりを投げてはいけない。

*1:配列を返した方が色々良さそうですが、そこは一旦置いておきます。