PHPで無限リストを作る

PHPで range(0, 10000000); とかやるとメモリ不足で死んでしまうので、無限ループするイテレータを作ってみましょう。

<?php
 
class Stream implements \Iterator
{
    private $position;
    private $offset;
    private $limit;
 
    public function __construct($offset, $limit = null)
    {
        $this->position = $offset;
        $this->offset   = $offset;
        $this->limit    = $limit;
    }
 
    public function current()
    {
        return $this->position;
    }
 
    public function next()
    {
        $this->position++;
    }
 
    public function valid()
    {
        if ($this->limit && $this->position > $this->limit) {
            return false;
        }
 
        return true;
    }
 
    public function key()
    {
        return $this->position;
    }
 
    public function rewind()
    {
        $this->position = $this->offset;
    }
 
    public function take($n)
    {
        return new \LimitIterator($this, 0, $n);
    }
}

無限ループさせる。

<?php
$stream = new Stream(0);
 
foreach ($stream as $i) {
    echo $i . "\n";   
}

=>
// 0
// 1
// 2
// 3
// 4
// .
// .

範囲指定してみる。

<?php
$stream = new Stream(50, 55);
 
foreach ($stream as $i) {
    echo $i . "\n";
}
 
=>
// 50
// 51
// 52
// 53
// 54
// 54

LimitIteratorを使えば無限リストから必要な数だけ取り出すことが出来ます。今回はtakeメソッドでラップしました。

<?php
$stream = new Stream(0);
 
foreach ($stream->take(5) as $i) {
    echo $i . "\n";
}
 
=>
// 0
// 1
// 2
// 3
// 4

しかしこれだけだとあまり使い道がないので、せめてmap機能は欲しい気がしますね。イテレータオブジェクトにはarray_系の関数が使えませんので、LazyMapIteratorを作ってみます。

<?php
class LazyMapIterator implements \Iterator
{
    protected $iterator;
    protected $callback;
 
    public function __construct(\Iterator $iterator, callable $callback)
    {
        $this->iterator = $iterator;
        $this->callback = $callback;
    }
 
    public function getIterator()
    {
        return $this->iterator;
    }
 
    public function current(){
        $f = $this->callback;
        return $f($this->iterator->current());
    }
 
    public function next()
    {
        $this->iterator->next();
    }
 
    public function key()
    {
        return $this->iterator->key();
    }
 
    public function valid()
    {
        return $this->iterator->valid();
    }
 
    public function rewind()
    {
        $this->iterator->rewind();
    }
}

Streamクラスにmapメソッドを生やします。

<?php
public function map(callable $f)
{
    return new LazyMapIterator($this, $f);
}

mapメソッドを使ってみます。

<?php
$stream = new Stream(1, 5);
$result = $stream->map(function($i) {
    return $i * 10;
});
 
var_dump(iterator_to_array($result));
 
array(5) {
  [1] =>
  int(10)
  [2] =>
  int(20)
  [3] =>
  int(30)
  [4] =>
  int(40)
  [5] =>
  int(50)
}

ちなみにPHPはSPLで色々なイテレータが用意されていますので、PHPの残念な配列操作にイラついている方は是非覗いてみてください(CallbackFilterIteratorを使えばfilter機能もすぐ実装できます)