linq.js ver 1.3.0.2 - CascadeDepthFirst/BreadthFirst

linq.jsにCascadeDepthFirst, CascadeBreadthFirstを追加しました。ちょっと分かり辛いのですが非常に強力です。深さ優先/幅優先で多段SelectManyをかけていくようなイメージ。説明しづらいので例をどうぞ。

<div id="tree">Root
    <div>Spine
        <div>Neck
            <div>Head</div></div>
        <div>RClavicle
            <div>RUpperArm
                <div>RLowerArm
                    <div>RHand</div></div></div></div>
        <div>LClavicle
            <div>LUpperArm
                <div>LLowerArm
                    <div>Hand</div></div></div></div></div>
    <div>RHip
        <div>RUpperLeg
            <div>RLowerLeg
                <div>RFoot</div></div></div></div>
    <div>LHip
        <div>LUpperLeg
            <div>LLowerLeg
                <div>LFoot</div></div></div></div></div>

こんなHTML、というかツリーに色々と操作することにします。例えばinnerTextのように、深さ不明のchildNodesを全て掘ってTextNodeだけを取り出して値を連結するとしたら、通常は再帰を使いますよね。でも、CascadeDepthFirstを使えば

var root = document.getElementById("tree");
var innerText = E.Make(root)
    .CascadeDepthFirst("$.childNodes")
    .Where("$.nodeType == 3")
    .Select("$.nodeValue")
    .ToString();

こんな感じに書けます。rootから深さ優先探索で子ノードを全取得、そのうちnodeTypeが3のもの(TextNode)のnodeValueを文字列連結。宣言的に、再帰よりも分かりやすく書けます。何より後段で豊富なLinqメソッドを使って値を操作していけるのが利点になります。今回から新しく追加されたMake(hoge)というのはRepeat(hoge,1)と等しい。Fromを用いるとオブジェクトはKeyValuePairに、文字列は一文字ずつのシーケンスに化けてしまうので、今回からこのメソッドを用意しました。

実のところMakeもCascadeDepthFirstもC# 3.0 Supplemental Library: Achiral - NyaRuRuの日記のパクりです(AchiralではMakeはMake.Sequence)。ぶっちゃけ今までも勝手に散々パクッているので(ごめんなさい、ありがとうございます。ライセンス的にアレっぽいので、どこかに書いておかないと……) むしろ何で今更追加なのかというと、あまりにも露骨すぎて劣化コピーを入れるのは失礼に思っていて。結局入れてしまったけれど。そんなわけで応用的なものを一つ。引き続き↑のDOMツリーを使って……

var root = document.getElementById("tree");
E.From(root.childNodes)
    .Select(function(child) { return { child: child, parent: root} }) // 外部の値を取りこむ時は文字列ラムダ式は使えません
    .CascadeDepthFirst(function(pair) // 中を入れ子で親の値を参照したいので文字列ラムダ式は不可
    {
        return E.From(pair.child.childNodes)
            .Select(function(child) { return { child: child, parent: pair.child} });
    }, "v,n=>{value:v,nestLevel:n}") // CascadeDepth/BreadthFirstの第二引数はネストレベルを利用可能
    .Where("$.value.child.nodeType == 1") // ELEMENT_NODEだけを取得するためフィルタ
    .WriteLine(function(t)
    {
        return t.nestLevel + ':'
            + E.From(t.value.child.childNodes).First().nodeValue + ' . '
            + E.From(t.value.parent.childNodes).First().nodeValue;
    });

例もパクりです。与えられた木から,子→親への対応を作る,を C# で - NyaRuRuの日記を、HTMLだったらツリーはDOMだよね、という感じで書いてみました。第二引数にはネストレベルを取得できるリザルトセレクターが使えます(省略も当然可能)。んー、DOMだとTextNodeが混ざるせいで、例として不適切に処理が混沌としてしまいました。あ、WriteLineの部分は子要素のFirstがTextNodeであると決め打ちしてます。正確にやりたいなら冒頭のinnerTextの例のようにWhereでTextNodeのみに絞った上で、ToStringで連結したほうが良いですね。

CascadeDepthFirstをCascadeBreadthFirstに変えると(両者は探索方式が違うだけで引数や戻り値は一緒)、こんな結果になります。

動作の差異がよく分かるんじゃないかなーと思います。

continue/break

ForEachでcontinue/breakを使えるようにしました。

E.Range(1, 100).ForEach(function(i)
{
    if (i % 2 == 0) return true; // continue
    if (i == 7) return false;    // break
    alert(i); // 1,3,5
});

灯台下暗しというか、こういうのって自分が必要になるまで気付かないというか。Linqって基本的に前段階のWhereで絞るからこの辺のものって必要になる機会が少ないんですよね。いや、言い訳ですけど。で、まあ、jQueryと同じでtrueをreturnすればcontinue、falseをreturnすればbreakになります。実装は超単純。

while (enumerator.MoveNext())
{
    if (action(enumerator.Current, index++) === false) break;
}

ActionなのにFuncになってしまった!しまった!と、思わなくもないけど、この辺が後付けでグダグダになるのはしょうがないので気にしないことにしよふ。じゃなくて、基本はActionなのでヨシとしておこう。むしろこの辺はJavaScriptがユルフワなのでC#ルールに(用語を)当てはめようとして無理が出ているだけ。そういえばで、個人的にはreturn;でcontinue、return true;でbreakにしたいなあ、とか思ったり思わなかったりなのだけど、その辺は標準に合わせた方が混乱しなくていいよねー、と思うことにしました。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive