「1から100の整数を出力してください。ただし5つの異なる方法を用いて」をPHPで

Posted by Hiraku on 2014-08-27

ちょっと遅れた感がありますが、先日のLL Diverに行ってきました。

forやめろ - LL Diver 2014 LT枠 from Esehara Shigeo

LT枠の「forやめろ」なんですが、「1から100までの整数を出力してください。ただし5つの異なる方法を用いて」というお題が出てきます。

これが今になって気になってきたのでPHPで解いてみようと思います。 たぶん趣旨的に、最適解とかじゃなくて、いろんな書き方をした方が得点高そう。

1. forとインクリメント

forやめろって言われてるのですが、とりあえずforです。 Duff's Deviceとかで展開しない限りは、これが最速解になると思われます。(100個ぐらいなら違いはないだろうけど。)

<?php
for ($i=1; $i<=100; ++$i) {
    echo $i, PHP_EOL;
}

やってることが出力だけなので、もう少し縮められますね。

<?php
for ($i=1; $i<=100; print $i++ . PHP_EOL);

echoはforの()の中に書けませんが、printは戻り値があるので式として書くことができます。

この辺をwhileとかdo whileで書きなおしても回答としてつまらないので、単純なfor系はこれぐらいにしておきます。

2. range()

1..10みたいな演算子はPHPにはありませんが、range関数で連続する値を持つ配列を作ることはできます。これ系の引数はよく悩みますが、rangeは「始まりの値」「終わりの値」を指定します。

<?php
foreach (range(1, 100) as $i) {
    echo $i, PHP_EOL;
}

こっちはforと違って最初に大きな配列をメモリ上に作るため、富豪的です。100程度なら大したことはないでしょうけど。

ちなみに亜種としてarray_walk()で回したり、

<?php
array_walk(range(1, 100), function($i){
    echo $i, PHP_EOL;
});

逐一出力しろとは書いてないので、連結してから出力することもできますね。

<?php
echo implode(PHP_EOL, range(1, 100));

これが思いつく限り一番短いかな。

3. 0-9を2つ組み合わせる(直積演算)

ここまで、だいたい普通の書き方だったので、無理やり感のあるのをいくつか考えてみます。forというか、「1-100の整数」の定義って何だろう?みたいな方向で考えます。

お題で出力しろと言われているのは1から100までですが、別に見た目が数字であればよくて、文字列で組み立てても大丈夫でしょう。

0-9の組を2つ組み合わせ、直積演算(CROSS JOIN的なやつ)をすると、00-99を作ることができます。 これに1を足せば1-100が出来上がります。

<?php
foreach (range(0, 9) as $i) {
    foreach (range(0, 9) as $j) {
        echo "$i$j" + 1, PHP_EOL;
    }
}

「文字列として結合して1を足す」って書き方は弱い型付けならではで、かなり気持ち悪いですね。。暗黙の型キャストが嫌なら10*$i + $j + 1とかですかね。

SQLだと「おー集合演算っぽい」ってなるんですが、手続型で書くと、単にループを2重にしただけの普通の見た目になってしまいました。。地味。

参考: SQLで数列を扱う (1/4):CodeZine

4. reduceで数え上げる

部屋に何人いるかとか、箱の中のケーキの個数とか、何か数える時は、「ひとつ、ふたつ…」と指折り数えるものではないでしょうか。

この様子をそのままプログラムに落とし込むのはどうでしょう。

まず100個の1を含む集合を用意します。これを順番に足していき、中間報告をさせてやれば、1-100の出力ができそうです。

<?php
array_reduce(array_fill(0, 100, 1), function($carry, $item){
    echo $current = $carry + $item, PHP_EOL;
    return $current;
});

reduceで副作用のある命令を書くのはどうなんだ、って気もしますが、一応動作します。

5. 漸化式と制限演算

1-100ということは、自然数の最初の100個ってことです。

自然数って、ある値に1を足したら次の値が求まるので、漸化式で書くとこんな感じでしょうか。

  • x(1) = 1
  • x(n+1) = x(n) + 1

これをそのままプログラムに落とし込むと、「自然数全体」を表す仮想的な集合を作ることができます。 これに制限を加えて最初の100個だけを取り出すと、1-100の整数が出来上がります。

(冷静に考えると、めっちゃ遠回りしてるだけなんですが…)

PHPで仮想的な集合と言うと、要するにイテレータかジェネレータのことになります。まずはイテレータ版。

<?php
class NaturalNumberIterator implements Iterator
{
    private $i = 1;
    function current() { return $this->i; }
    function key()     { return $this->i; }
    function next()    { ++$this->i; }
    function rewind()  { $this->i = 1; }
    function valid()   { return true; }
}

$it = new NaturalNumberIterator;
$it = new LimitIterator($it, 0, 100);

foreach ($it as $i) {
    echo $i, PHP_EOL;
}

NaturalNumberIteratorだけだと、1から無限に数字を作っていってしまいます。(実際はPHPのinteger型は無限精度ではないので、PHP_INT_MAXを超えるとfloatに変わって動きがおかしくなります)

LimitIteratorはイテレータのうち任意の指定した区間を取り出してくれます。こっちはrangeと違って「読み飛ばす個数」「取り出す個数」が第2第3引数です。

「ある性質を持つ集合」を定義して、「制限演算」を作用させて、目的のものを得る。 見た目はあんまりイケてないですが、ちょっと宣言型っぽくなりました。

ちなみにジェネレータだとこうなります。 こっちの方がやっぱり読みやすいですね。

<?php
function naturalNumbers($i = 1) {
    for (;;) yield $i++;
}

$it = naturalNumbers();
$it = new LimitIterator($it, 0, 100);

foreach ($it as $i) {
    echo $i, PHP_EOL;
}

飽きてきたのでまとめ

ぶっちゃけ1-100を出力するぐらいならforかrangeでいいし、それ以外の書き方をしていたらコードレビューで文句言われると思います。

ただ、イテレータとかジェネレータは抽象度が高いので、ループ構文の中にごちゃっと書くより、きれいに書ける可能性があります。まあ使い方を覚えておけばプログラマとして幅が広がるんじゃないでしょうか。(LTの趣旨はそういうところですよね)

関連:PHP - コードをまとめる技術としてのイテレータとジェネレータ - Qiita

あとはGinqとかUnderscore.phpでも書けそうかなー。

PHPの最新記事