linqの原理を深く探究する--どのように自分の言語の中でlinqを実現するか

6581 ワード

穴は少し掘られていますが、最近はscalaにlinqを加えるつもりです.
スパークRDDやDataFrameで直接使うのはエイズではないでしょうか.
c#を何度か使ったことがありますが、linqはとても直感的で、このデザインが大好きです.でも今は忘れ物が少ないので、linqはいったい何なのか振り返ってみましょう.
overview
タイプ署名の拡張方法の詳細はさておき、公式サイトから与えられた最も基本的な例から、linqが何なのかを全体的に概説します.
using System;
using System.Linq;

class IntroToLINQ
{        
    static void Main()
    {
        // The Three Parts of a LINQ Query:
        //  1. Data source.
        int[] numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 };

        // 2. Query creation.
        // numQuery is an IEnumerable
        var numQuery =
            from num in numbers
            where (num % 2) == 0
            select num;

        // 3. Query execution.
        foreach (int num in numQuery)
        {
            Console.Write("{0,1} ", num);
        }
    }
}

linq、言語統合クエリーは、文法的にクラスsqlをサポートするクエリー構文であり、sqlクエリーを熟知している広範なcoderに対して、チェーンメソッド呼び出しよりも可読性が高く、どこへ行ったのか分からない.
しかし、これは文法糖にすぎず、コンパイル後も方法呼び出しに変換しなければならない.
たとえば、上記のクエリは次のようなものです.
var numQuery = numbers.Where(num => num %2 ==0).Select(num => num);

もちろんselectのデータは変わらないので、このselect呼び出しは完全に省略できます.Whereはfilterに相当し、Selectはmapに相当し、これらの簡単な操作は非常に理解しやすい.linqでサポートされている他のjoin、aggerateなどのオペレータは、同じように書く方法で、理解しやすいです.
特殊なのは、複数のfromがつながっていると、少し複雑になります.このような状況を具体的に紹介します.
fromとSelectMany
まず例をあげます.
using System;
using System.Linq;

class IntroToLINQ
{
    static void Main ()
    {
        int[] numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 };

        var numQuery =
            from num0 in numbers
            from num1 in numbers
            where  num0 + num1 > 11
            select new  {num0, num1};

        foreach (var num in numQuery) {
            Console.Write ("{0,1} ", num);
        }
    }
}

クエリー文だけを見て、numbersとnumbers(自分と自分)のデカルト積の中で2つの数の和が11より大きい組み合わせを選択し、出力は:
{ num0 = 5, num1 = 6 }
{ num0 = 6, num1 = 5 }
{ num0 = 6, num1 = 6 }

少し拡張した内容を話して、読めなくても大丈夫です.スキップしてもいいです.興味があればhaskellのMonadを理解してください.
scalaのコードを見ています.
object Main {
  def main(args: Array[String]): Unit = {
    val numbers = List(0, 1, 2, 3, 4, 5, 6)

    val queryResult =
      for (num0  10) yield (num0, num1)

    queryResult.foreach(println)
  }
}

出力結果:
(5,6)
(6,5)
(6,6)

次にghciで実行できるhaskellコードです.
numbers = [0, 1, 2, 3, 4, 5, 6]

queryResult = do {
  num010
    then [(num0,num1)]
    else []
}

haskellはforeachがなくて、手動で結果を見てみます
> queryResult

に届く
=> [(5,6),(6,5),(6,6)]

結果がそっくりであることがわかりますが、実はこの3つはもともと同じことです.c#直列のfromクエリー式、scalaのfor文法、haskellのdo notationは、本質的に一つのものであり、文法糖であり、Monadの操作を直列に接続し、最後に方法/関数呼び出しに翻訳される.その下位メソッド/関数は、それぞれSelectManyflatMap>>=である.Monadは何なのか、また別の話題ですが、大きくないと言っても小さくない話題です...
本題に戻り、これらの容器を配列するSelectMany法の具体的な機能を見てみましょう.
関数を受け入れるSelectMany
まず、最も一般的なSelectManyの例を見てみましょう.これも本来の意味です.
int[] list = { 1, 2, 3 };

Func selector = x =>
{
    var s = String.Format("hi{0}", x);
    return new[] { s, s, s };
};

var result = list.SelectMany(selector);

foreach (var e in result)
{
    Console.Write("{0},", e);
}

出力:
hi1,hi1,hi1,hi2,hi2,hi2,hi3,hi3,hi3,

簡単に理解できますが、SelectManyはコンテナの各要素に1回適用し、毎回新しいコンテナを返します.例えば、selectorは数字Nを受け入れて{"hiN","hiN","hiN"}というリストを返し、要素ごとに呼び出すとこの大きなリスト{{"hi1","hi1","hi1"},{"hi2","hi2","hi2"},{"hi3","hi3","hi3"}}が得られ、最後にこの大きなリストを平らにすると最終的な結果が得られる:{hi1,hi1,hi1,hi2,hi2,hi2,hi3,hi3,hi3}.もちろん実際の実現は必ずしもそうではないが、このように理解すれば正しい.
この関数の署名を見てみましょう.
public static IEnumerable SelectMany(
    this IEnumerable source,
    Func> selector
);

例のタイプに対応しています.
c#に触れたことがない場合は2点に注意してください.
  • の最初のパラメータsourceの前にthisがあります.これは拡張方法の構文であり、source.SelectMany(selector)SelectMany(source,selector)に相当します.
  • IEnumerableはインタフェースであり、c#の配列はこのインタフェースを実現している.したがって、int[]IEnumerablestring[]を満たし、IEnumerableを満たす.

  • このSelectManyは何の役に立ちますか?
    前述したように、一連の操作を直列に接続することができます.もう1つの例を見て、2つのリストのデカルト積を求めます.例えば、[1,2] he [3,4]に対して、[(1,3),(1,4),(2,3),(2,4)]を得るにはどうすればいいですか.
    int[] alist = { 1, 2 };
    int[] blist = { 3, 4 };
    
    var result = alist.SelectMany(
        a => blist.Select(
            b => new { a, b }
        )
    );
    
    foreach (var e in result)
    {
        Console.Write("{0},", e);
    }
    

    出力:{ a = 1, b = 3 },{ a = 1, b = 4 },{ a = 2, b = 3 },{ a = 2, b = 4 },どのように理解して、中の関数の機能を見てもいいです.alistの1つの要素aに対して、blistの各要素と組み合わせて最後に[(a,3),(a,4)]を生成します.alistのaごとにこの関数を1回来ると[ [(a0,3),(a0,4)], [(a1,3),(a1,4)] ]が得られ、それを平らにするのが最後の結果です.
    次に、3つのリストのデカルト積を求めます.
    int[] alist = { 1, 2 };
    int[] blist = { 3, 4 };
    int[] clist = { 4, 5 };
    
    var result = alist.SelectMany(
        a => blist.SelectMany(
            b => clist.Select(
                c => new { a, b, c }
            )
        )
    );
    
    foreach (var e in result)
    {
        Console.Write("{0},", e);
    }
    
    { a = 1, b = 3, c = 4 },{ a = 1, b = 3, c = 5 },{ a = 1, b = 4, c = 4 },{ a = 1, b = 4, c = 5 },{ a = 2, b = 3, c = 4 },{ a = 2, b = 3, c = 5 },{ a = 2, b = 4, c = 4 },{ a = 2, b = 4, c = 5 },
    

    2つの関数を受け入れるSelectMany
    まず例を見てみましょう.
    int[] ilist = { 1, 2 };
    double[] dlist = { 0.1, 0.2, 0.3 };
    
    Func collectionSelector =
        _ => dlist;
    Func resultSelector =
        (int_num, double_num) => String.Format("'{0}'", int_num + double_num);
    
    var result = ilist.SelectMany(collectionSelector, resultSelector);
    foreach (var e in result)
    {
        Console.Write("{0},", e);
    }
    

    結果:
    '1.1','1.2','1.3','2.1','2.2','2.3',
    

    翻訳ルール
    では、メソッド呼び出し形式を書きます.
    numQuery1 = numbers.SelectMany (
        _ => numbers,
        (num0, num1) => new {num0, num1}
    ).Where( x => x.num0 + x.num1 >11);