Added Ginq::iterate(), Ginq::unfold()
akanehara opened this issue · 19 comments
いるよね
欲しいね
前に聞いたとき、unfoldに終端条件用意せず、無限シーケンスするといっていたけど、
途中できる場合も、末尾の次の要素を作る必要になる。
数値列なら気にならないだろうけど、コストの重いオブジェクト生成だと、気になると思うので、やっぱりOptional型でくるんだほうがいい気がする。
Optionalが正しいのは間違いないけどPHP的にはやりすぎじゃないかしらと考えてlinq.js に倣って打ち切りを takeWhile に委ねよう、と考えてました。(もしSPLあたりにOption[A]相当品があるなら話は別)
でも、本件とは無関係にScalaでいうOption[A]みたいなクラスがPHPでも欲しくなる局面があるにはあるから、($x->getOrElse($alt)とか欲しいでしょ?)作ってもいいのかもしれない、とも。
引数でもすでに null は特別扱いだし、いっそ null を None とみなすのはどうだろうか?
Optionは集合(イテレーション可能なもの)というより値(イテレーションできないもの)のレイヤーの問題じゃないかと思ったり
iterate(a, a -> a) ではなく unfold(b, b -> [a, b]) のほうでは [] を Some、null を None とみなせば何の問題も起こさないないことに気がついた。[$x, $acc] で続行。null で打ち切り。
もうひとつは、list を option のように使いうるという考え方を PHP の array でやっちゃう。つまりiterateならばシグニチャを iterate(a, a -> [a]) として、[a]の要素数が1なら Some、0 なら Noneというわけ。
このルールを unfold についても生真面目に適用すると、タプルがないぶん unfold(b, b -> [[a, b]]) となってうざいけどね。
ちょっと iterateのことは置いといて、unfold を「ジェネレータ関数が 2 要素の配列を返す限り生成を続ける」というので feature ブランチ切ってみよう。
おっと seed -> [v, seed] だと次のシードの計算が早すぎるのか。じゃあ、seed -> [v, () -> seed] ?
自力で打ち切る unfold なら seed が Eagar だろうと Lazy だろうとたいした違いはないけど、takeWhile で外から打ち切るケースでは seed が Eagar だと 最後の seed の計算が無駄になる??
iterationの仕方によるだろうけど、
closureの戻り値が、[後続のnext()対象, 次回のiteration]であるのなら、無駄になると思う。
どの程度無駄になるかは、使う側が返すObjectに依存するため、ライブラリ作成者には見積もれない。
unfold戻り値の二番目がLazyなのは使う側からしたら扱いづらいと思う。
高階関数が高階関数を返す形になるから。
ただしiterate()内部の実装の詳細としてlazyなunfoldを使うのは問題ない。
まさにそれで決着しそう。seed を Lazy にするかどうかは、Ginq::unfold の使い手まかせ。
そのうえで Ginq::iterate は、 seed が Lazy な Ginq::unfold で実装。
/**
* @param mixed $seed
* @param \Closure $generator seed -> ([v, seed] | null)
* @return Ginq
*/
public static function unfold($seed, $generator)
{
return self::from(self::$gen->unfold($seed, $generator));
}
/**
* @param mixed $initial
* @param \Closure $generator v -> v
* @return Ginq
*/
public static function iterate($initial, $generator)
{
return self::unfold(
$initial,
function($seed) use ($generator) {
$v = FuncUtil::force($seed);
return array(
$v,
function () use ($v, $generator) { return $generator($v); }
);
}
);
}
おまけ
JavaのReflectionでちまちま親クラスたどるの面倒で、unfold作ったんだけど、
自分で打ち切らない場合、最後の戻り値が[null, null.getSuperClass()]となってしまうため、unfoldのclosureで次の状態を作る前に明示的なnullチェックが必要となり、unfoldのうまみが減る気がする。
このことも、打ち切りをunfoldの外にはしない方がいいと考えている理由の一つ。
// 明示的なnullチェック
// 多分こんな利用の仕方になってしまう
// うざい・・・
Ginq.unfold(obj.getClass(), function(seed) { return [seed, seed != null ? seed.getSuperClass() : null]; });
Lazyにすれば、nullチェックを省略できるんだろうけど、
高階関数の高階関数はちょっと・・・( ,,Ծ ‸ Ծ,,)
JavaのCollection Streaming APIがiterate()としているので、この名前を採用したんだろうけど、正直いい名前とは思えない(個人的な感想です)。
無限数列を作るので、〜Infinity()とか、単純にinfinity()とかどんなもんでしょうか?
ちなみにちなみに、F#ではinitInfinite(int -> T)で、iter は forEach的な操作となっておりまする。
おっと、ScalaのStreamもHaskellのData.Listもiterate
なので、考えもしなかった:sweat_smile:
通りがいいのはあればそれを採用したいかなあ。
おっと、それ以前に take が無駄なフェッチしてることに気がついた。これは別 Issue で。
ScalaのStreamもHaskellのData.Listもiterateなので
むしろF#が異端であったか・・・