LoginSignup
46
44

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-09-27

はじめに

この記事は 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 というものもありますが、今回は触れません。

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 の生成よりもメモリに優しい実装を行うことが可能になる
46
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
46
44