ToLookup<TKey,TElement> の基本と応用

ネタもと『どうでもいいコードの断片 - プログラマブルな趣味、もっぱらスクリプティング』.
ツボにはまればとっても便利な ToLookup.でもちょっと知名度低いかもしれない.
例えばこんなコード.

using System;
using System.Linq;
using System.IO;

static class Program
{
    static void Main(string[] args)
    {
        // ファイルリスト
        var fileNames = new[]
        {
            "200.xls",
            "100.doc",
            "300.ppt",
            "500.xls",
            "400.ppt",
            "600.ppt"
        };

        // 拡張子からファイルリストへのルックアップテーブルを作成
        var extToFiles = fileNames.ToLookup(f => Path.GetExtension(f));

        // 拡張子 ".xls" を持つファイル名を表示
        extToFiles[".xls"].ToList().ForEach(Console.WriteLine);

        // 拡張子 ".js" を持つファイル名を表示
        extToFiles[".js"].ToList().ForEach(Console.WriteLine);
    }
}

ILookup<TKey,TElement> って何?

ToList や ToArray,ToDictionary は今までも対応するコンテナがあったからまあ分かる.でも ToLookup が返す Lookup<TKey, TElement> ってコンテナは何? という人は IDictionary<TKey, IEnumerable<TElement>> の便利版と考えればとりあえず OK.

ILookup<string, int> lookup = ...

// インデクサにキーを与えると対応する IEnumerable<T> が返る
IEnumerable<int> q1 = lookup["Key1"];
IEnumerable<int> q2 = lookup["Key2"];

IDictionary<TKey, IEnumerable<TElement>> と比べてどこが便利かというと,存在しないキーについて空のシーケンスを返してくれるところ.

// 存在しないキーについては空シーケンスが返り,例外は起きない
IEnumerable<int> q2 = lookup["not existing key"];
Assert.AreEqual(0, q2.Count());

存在しないキーに空シーケンスを返すと何がうれしいの?

この挙動はパイプラインを連結していったときに真価を発揮する.

// 以下のようなコードで key の存在チェックが不要になる
List<string> list = ...

list.Select(key => lookup[key])
    .Select(q => q.Count());

list.SelectMany(key => lookup[key])
    .ForEach(Console.WriteLine);

どんなときに使うの?

データベースによる階層構造表現.ありがちな隣接リストモデルでたとえばこんなのを考えてみる.

Element Parent
"Object" null
"MarshalByRefObject" "Object"
"Stream" "MarshalByRefObject"
"BufferedStream" "Stream"
"FileStream" "Stream"
"IsolatedStorageFileStream" "FileStream"
"MemoryStream" "Stream"
"UnmanagedMemoryStream" "Stream"
"CryptoStream" "Stream"
"PipeStream" "Stream"
"AnonymousPipeServerStream" "PipeStream"
"AnonymousPipeClientStream" "PipeStream"
"NamedPipeServerStream" "PipeStream"
"NamedPipeClientStream" "PipeStream"

階層ツリーを親の方向にたどるのは簡単.要素名は Unique ということにして,ある要素の階層の親をたどるには再帰的に Parent 列の項目をたどっていけば OK.でも Root から子方向に全部たどるのはこのままだと分かりにくいね,と.
そこで Parent をキーにして "ToLookup" してみる.

  • Object
    • MarshalByRefObject
  • MarshalByRefObject
    • Stream
  • Stream
    • BufferedStream
    • FileStream
    • MemoryStream
    • UnmanagedMemoryStream
    • CryptoStream
    • PipeStream
  • FileStream
    • IsolatedStorageFileStream
  • PipeStream
    • AnonymousPipeServerStream
    • AnonymousPipeClientStream
    • NamedPipeServerStream
    • NamedPipeClientStream

これは親が同じ要素同士でテーブルを分割したのに相当する.最近の Explorer や Outlook の表示形態で「グループ化」ってのがあるけど,あれと同じ.
まず子要素を列挙して,列挙された子要素をキーとすれば今度は孫要素が列挙できる.こうして,ある要素にぶら下がった要素を全て列挙できる.

隣接リストモデルから ToLookup

まず C# 3.0 でテーブル定義.

var table = new []
{
    new {Element="Object", Parent=default(string)},
    new {Element="MarshalByRefObject", Parent="Object"},
    new {Element="Stream", Parent="MarshalByRefObject"},
    new {Element="BufferedStream", Parent="Stream"},
    new {Element="FileStream", Parent="Stream"},
    new {Element="IsolatedStorageFileStream", Parent="FileStream"},
    new {Element="MemoryStream", Parent="Stream"},
    new {Element="UnmanagedMemoryStream", Parent="Stream"},
    new {Element="CryptoStream", Parent="Stream"},
    new {Element="PipeStream", Parent="Stream"},
    new {Element="AnonymousPipeServerStream", Parent="PipeStream"},
    new {Element="AnonymousPipeClientStream", Parent="PipeStream"},
    new {Element="NamedPipeServerStream", Parent="PipeStream"},
    new {Element="NamedPipeClientStream", Parent="PipeStream"},
};

ToLookup を使ってルックアップテーブルに変換.

var lookup = table.Where(item => item.Parent != null)
                  .ToLookup(item => item.Parent, item => item.Element);

Parent が null のものを抜いておくのがこつ.ToLookup のオーバーロードもいくつかある
いったんルックアップテーブルができればあとは簡単.

// "Object" の子要素を列挙
lookup["Object"].ForEach(s => Console.WriteLine(s));

// "Object" の孫要素を列挙
lookup["Object"].SelectMany(item => lookup[item])
                .ForEach(s => Console.WriteLine(s));

// "Object" のひ孫素を列挙
lookup["Object"].SelectMany(item => lookup[item])
                .SelectMany(item => lookup[item])
                .ForEach(s => Console.WriteLine(s));

Lookup を利用した再帰を一発で書く

先日公開した Achiral では CascadeDepthFirst / CascadeBreadthFirst という Extension Method が用意してあって,先ほどの SelectMany の繰り返しを再帰的に行ってくれる.色々な型を "Extension" しているので,書き方も色々.

// Func<T, IEnumerable<T>>.CascadeDepthFirst(T initial)
var q1 = Make.Func((string item) => lookup[item])
             .CascadeDepthFirst("Object");

// ILookup<T, T>.CascadeDepthFirst(T initial)
var q2 = lookup.CascadeDepthFirst("Object");

// IEnumerable<T>.CascadeDepthFirst(ILookup<T, T> lookup)
var q3 = Make.Sequence("Object")
             .CascadeDepthFirst(lookup);

// IEnumerable<T>.CascadeDepthFirst(Func<T, IEnumerable<T>> func)
var q4 = Make.Sequence("Object")
             .CascadeDepthFirst(item => lookup[item]);

// IEnumerable<T>.CascadeDepthFirst(Func<T, IEnumerable<T>> func,
//                                  Func<T, int, TResult> selector)
var q5 = Make.Sequence("Object")
             .CascadeDepthFirst(item => lookup[item],
                                (item, nestLevel) => item);

実際に Reflection で mscorlib.dll と System.Core.dll に含まれる type-tree を書いてみる.ルートを System.IO.Stream として,それ以下のものを列挙.

using System;
using System.Linq;
using System.IO;

using Achiral;
using Achiral.Extension;

static class Program
{
    static void Main(string[] args)
    {
        // mscorlib.dll と System.Core.dll に含まれる全ての型
        var types = Enumerable.Concat(
                        typeof(int).Assembly.GetTypes(),
                        typeof(Enumerable).Assembly.GetTypes());

        var childrenLookup = types.Where(type => type.BaseType != null)
                                  .ToLookup(type => type.BaseType, type => type);

        childrenLookup.CascadeDepthFirst(typeof(Stream), (Type, NestLevel) => new { Type, NestLevel } )
                      .ConsoleWriteLine(item => new string(' ', 3 * item.NestLevel) + item.Type.FullName);
    }
}

実装についてはソースコードをどうぞ.

便利な ToLookup を追加する

Achiral では他にも ILookup を作るための拡張メソッドをサポートしている.
一番使い勝手が良いのが,IDictionary<TKey, IEnumerable<TResult>> から ILookup<TKey, TResult> を作る拡張メソッド.

using Achiral;
using Achiral.Extension;

var lookup = new Dictionary<string, IEnumerable<string>>
             {
                 { "Object", new []{"MarshalByRefObject"} },
                 { "MarshalByRefObject", new []{"Stream"} },
                 { "Stream", new []{"BufferedStream", "FileStream"} },
                 { "FileStream", new []{"IsolatedStorageFileStream"} },
             }.ToLookup();

このほか,IDictionary<TKey, Func<IEnumerable<TResult>>> から ILookup<TKey, TResult> への ToLookup も追加してある.

using Achiral;
using Achiral.Extension;

var lookup = new Dictionary<string, Func<IEnumerable<object>>>
             {
                 { "Once", () => Enumerable.Repeat(new object(), 1) },
                 { "Twice", () => Enumerable.Repeat(new object(), 2) },
                 { "Infinite", () => Make.Repeat(new object()) },
             }.ToLookup();

これで階層的な構造のデータソースも比較的楽に取り扱えるようになる,かもしれない.