PHP でイテレータを使って関数型プログラミングっぽい雰囲気を出す: range 編

# はじめに

この記事は PHP カンファレンス 2014 における私 @yuya_takeyama の発表 *Good Parts of PHP and The UNIX Philosophy* のネタ出しも兼ねて書かれています。
当日の発表には、この記事に関する内容が出てくるかもしれませんし、出てこないかもしれません。
興味のある方は是非当日の発表の場にお越しいただければと思います。 ([参加登録](http://phpcon.doorkeeper.jp/events/15532))

なお、この記事は何回かに分けて書かれ、今回は関数型っぽい雰囲気が出るところまで達していません。

今回実装したコードは以下の GitHub リポジトリに含まれています。
(今後の記事のネタバレ含む)

GitHub: [yuya-takeyama/functional_programming_in_php](https://github.com/yuya-takeyama/functional_programming_in_php)

# イテレータとは何か

イテレータとは値の連続です。
もっとざっくり言うと配列のような何かです。
ただし、添字を使った各要素へのアクセス (`$foo[0]` とか `$foo['key']` とか) は含まない点に注意が必要です。

動詞型の `iterate` を英和辞典で引くと、「~を反復する」とか「~を繰り返す」などと訳されます。
なので `iterator` は「~を反復するもの」とか「~を繰り返すもの」などといった感じでしょうか。

プログラミングにおいては、ループ処理の抽象化に用いられ、古くは Gang of Four のデザインパターンとして `Iterator pattern` が紹介されています。

Wikipedia: [Iterator pattern](http://en.wikipedia.org/wiki/Iterator_pattern)

# PHP におけるイテレータ

PHP おいてイテレータとは、 `Iterator` インターフェイスを実装したオブジェクトはイテレータとなり、`foreach` 文でループ処理を行うことができます。

PHP.net: [Iterator インターフェイス](http://php.net/manual/ja/class.iterator.php)

PHP 5.0 で登場し、かれこれ 10 年経ちますが、未だに広く理解されていないのかもしれません。
(私も含め)

このインターフェイスには以下の 5 つのメソッドを必要とします。
これらを正しく実装することで、`foreach` 文でループ可能なイテレータとなります。

メソッド |用途|
--------|-----------------------------------------------------------------------------------------|
rewind()|イテレータを初期状態に巻き戻す。繰り返し処理開始時に呼び出される。                               |
key()   |繰り返し処理における現在のキーを返す。`foreach ($iterator as $key => $value)` における `$key` |
current()|繰り返し処理における現在の値を返す。`foreach ($iterator as $key => $value)` における `$value`|
next()   |繰り返し処理を次に進める。各繰り返しの最後に呼び出される。                                     |
valid()  |次の要素が存在すれば `true`、存在しなければ `false` を返す。各繰り返しの前に呼び出される。       |

厳密には `Traversable` インターフェイスを実装したものが、PHP において `foreach` 可能なイテレータで、`Traversable` なインターフェイスには `IteratorAggregate` というものもありますが、今回は触れません。

* PHP.net: [Traversable インターフェイス](http://php.net/manual/ja/class.traversable.php)
* PHP.net: [IteratorAggregate インターフェイス](http://php.net/manual/ja/class.iteratoraggregate.php)

# range() をイテレータとして実装する

`Iterator` インターフェイスを実装したイテレータの例として、数値の範囲を表す `RangeIterator` を実装しましょう。

PHP には組み込みで `range()` という関数があり、`range(1, 5)` とすると `array` として `[1, 2, 3, 4, 5]` が返ります。
デフォルトではこのように値が 1 ずつ増えて行きますが、第三引数に指定することもできます。

`range(1, 10, 2)` とすると、`[1, 3, 5, 7, 9]` が返ります。

これはこれで必要最低限の機能を持った便利な関数ですが、以下のような問題があります。

* 呼び出し時に全ての要素を持った `array` を生成するため、要素数分のメモリを確保してしまう
* 無限数列を扱うことができない

`RangeIterator` はこれらの問題を解決します。

```php
namespace Yuyat\Functional\WithIterator;

class RangeIterator implements Iterator
{
    private $start;
    private $end;
    private $step;
    private $key;
    private $current;

    public function __construct($start, $end, $step = 1)
    {
        $this->start = $start;
        $this->end   = $end;
        $this->step  = $step;
    }

    public function rewind()
    {
        $this->key     = 0;
        $this->current = $this->start;
    }

    public function next()
    {
        $this->key += 1;
        $this->current += $this->step;
    }

    public function key()
    {
        return $this->key;
    }

    public function current()
    {
        return $this->current;
    }

    public function valid()
    {
        return $this->current() <= $this->end;
    }
}

function range($start, $end, $step = 1) {
    return new RangeIterator($start, $end, $step);
}
```

毎回 `new RangeIterator(1, 10)` などと書くのは冗長なので、ヘルパー関数として `range()` も用意しました。
PHP 標準の `range()` 関数と同じ引数ではあるものの、配列ではなくイテレータを返すようになっています。

# 配列を返す range() とイテレータを返す range() の比較

まずはメモリ消費量から検証してみましょう。
検証には PHP 5.6 を使用していますが、5.3 以降なら動くつもりです。

変数 1 から 10 万までの `range` を作ってそれを全て出力し、その前後でのメモリ使用量を計測します。

## 配列を返す `range()` のメモリ消費量

以下のようなコードを用意します。

```php
<?php
printf("Initial memory usage = %.2fMB\n", round(memory_get_usage() / 1024 / 1024, 2));
$range = range(1, 100000);

foreach ($range as $n) {
    echo $n, PHP_EOL;
}

printf("Peak memory usage = %.2fMB\n", round(memory_get_peak_usage() / 1024 / 1024, 2));
```

これを実行すると以下のような結果となりました。

```
$ php memory_of_range_returns_array.php
Initial memory usage = 0.22MB
1
2
3
(中略)
99998
99999
100000
Peak memory usage = 14.19MB
```

これだけのコードで 14MB ものメモリを消費してしまっています。

## イテレータを返す `range()` のメモリ消費量

以下のようなコードを用意します。

```php
<?php
require_once __DIR__ . '/Functional.php';
use Yuyat\Functional\WithIterator as F;

printf("Initial memory usage = %.2fMB\n", round(memory_get_usage() / 1024 / 1024, 2));
$range = F\range(1, 100000);

foreach ($range as $n) {
    echo $n, PHP_EOL;
}

printf("Peak memory usage = %.2fMB\n", round(memory_get_peak_usage() / 1024 / 1024, 2));
```

これを実行した結果は以下の通りです。

```
$ php memory_of_range_returns_iterator.php
Initial memory usage = 0.27MB
1
2
3
(中略)
99998
99999
100000
Peak memory usage = 0.29MB
```

ほとんどメモリを消費することなく、処理を終えました!

# イテレータのまとめ

* イテレータはループ処理を抽象化したものである
* PHP におけるイテレータは `Iterator` または `IteratorAggregate` を実装したクラスである
* PHP におけるイテレータは `foreach` 文によりループできる
* イテレータを使うことで、`array` の生成よりもメモリに優しい実装を行うことが可能になる