はじめに
この記事は PHP カンファレンス 2014 における私 @yuya_takeyama の発表 Good Parts of PHP and The UNIX Philosophy のネタ出しも兼ねて書かれています。
当日の発表には、この記事に関する内容が出てくるかもしれませんし、出てこないかもしれません。
興味のある方は是非当日の発表の場にお越しいただければと思います。 (参加登録)
なお、この記事は何回かに分けて書かれ、今回は関数型っぽい雰囲気が出るところまで達していません。
今回実装したコードは以下の GitHub リポジトリに含まれています。
(今後の記事のネタバレ含む)
GitHub: yuya-takeyama/functional_programming_in_php
イテレータとは何か
イテレータとは値の連続です。
もっとざっくり言うと配列のような何かです。
ただし、添字を使った各要素へのアクセス ($foo[0]
とか $foo['key']
とか) は含まない点に注意が必要です。
動詞型の iterate
を英和辞典で引くと、「~を反復する」とか「~を繰り返す」などと訳されます。
なので iterator
は「~を反復するもの」とか「~を繰り返すもの」などといった感じでしょうか。
プログラミングにおいては、ループ処理の抽象化に用いられ、古くは Gang of Four のデザインパターンとして Iterator pattern
が紹介されています。
Wikipedia: Iterator pattern
PHP におけるイテレータ
PHP おいてイテレータとは、 Iterator
インターフェイスを実装したオブジェクトはイテレータとなり、foreach
文でループ処理を行うことができます。
PHP.net: Iterator インターフェイス
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 インターフェイス
- PHP.net: IteratorAggregate インターフェイス
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
はこれらの問題を解決します。
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
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
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
の生成よりもメモリに優しい実装を行うことが可能になる