Edited at

PHP: iterable型はiterator_to_array()に渡しちゃいけない

PHPにはiterable型という疑似型があり、arrayTraversableがこの型と互換している。iterable型は「foreachで回せるもの」を指す型と考えて良い。

例えば、次のように戻り値にiterableの型を宣言すると、配列を返してもTraversableを返すことになるジェネレーター構文(yield)を使ってもどちらでも構わないことになる。

interface FileList {

public function getList(): iterable;
}

final class ArrayFileList implements FileList {
public function getList(): iterable {
return ['a.php', 'b.php', 'c.php'];
}
}

final class GeneratorFileList implements FileList {
public function getList(): iterable {
yield 'a.php';
yield 'b.php';
yield 'c.php';
}
}


iterator_to_array関数はイテレータを配列に変換する

PHPにはiterator_to_array関数がある。これは「イテレータを配列にコピーする」関数だ。第一引数に「コピーしたいイテレータ」を渡すと、配列に変換して返してくれる。

$files = (new GeneratorFileList)->getList();

assert($files instanceof Generator);
assert(iterator_to_array($files) === ['a.php', 'b.php', 'c.php']);


iterator_to_array関数にiterable型は渡さないほうがいい

このiterator_to_arrayには注意点がある。iterable型は渡さないほうがいいという点だ。公式マニュアルをよく読むと分かるが、iterator_to_arrayの第一引数の型はTraversableでなければならないからだ。

Screenshot_2019_01_10_22_45.png

この関数の説明には「イテレータを配列にコピーする」と書いてあるので、一見するとforeachでイテレートできるものなら何でもいけそうな気がしてくるが、そこはドキュメントをちゃんと読まねばならない。Traversableiterableだが、その逆のiterableTraversableではないし、Iteratorでもない。この場合iterableIteratorは名前が似ているが別ものと考えたほうがいい

したがって、次のコードのように配列のiterable型の値を渡すと、「TypeError: Argument 1 passed to iterator_to_array() must implement interface Traversable, array given」というエラーが発生する。

$files = (new ArrayFileList)->getList();

assert(is_array($files));
iterator_to_array($files); // TypeError


iterable型をarray型に変換する方法

PHPには残念ながら、iterablearrayに変換する関数が無いので、次のような関数を自作する必要がある。

function iterable_to_array(iterable $iterable): array

{
$array = [];
\array_push($array, ...$iterable);
return $array;
}

この実装であれば、arrayiterable型を渡した場合でも、iterable型を配列に変換することができる。

$files = (new ArrayFileList)->getList();

assert(is_array($files));
assert(iterable_to_array($files) === ['a.php', 'b.php', 'c.php']);

本稿で取り上げた、iterator_to_arrayの実験コードはGitHubで公開しているので、試したい方はご覧ください。