akanehara/ginq

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#が異端であったか・・・