Functional Programming w/ C# LINQ

ここ 9 ヶ月ほど C# LINQ プログラミングをしてきて、関数型・集合論的演算はやはり素晴らしいと思ってきたが、コーディングの目的が調査・研究から運用目的のシステム構築に移りつつあるためか、LINQ の限界が見えてきた。

C# は unsafe で書けば C / C++ 級の低水準コーディングが、managed で書けば Java 級の高水準コーディングが、LINQ で書けば超高水準な関数型・集合論的コーディングが可能になり、可読性と開発効率が飛躍的に向上する。水準を選べるその幅広さと、OnMemory・RDB へのデータアクセスが透過的・無差別に記述できる LINQ という他の言語にない OR マッピングアプローチがすばらしいのだが……… unsafe → managed → LINQ と高水準になる過程で抽象化が図られているわけであり、抽象化とは実装を切り離すこと、実装はコンパイラあるいはランタイムが担い、プログラマが管理しなくてよい (≒できない) とすることであることを再認識した。

このことに思いを致した結果、「実装を切り離す」ということの弊害が目につくようになってしまった。

LINQ を多用してきたものの .GetEnumerator() は2、3度ほどコピペでしか書いたことがないため見過ごしてきたが、想定通りに動かないケースに対応しようと先週、ある .GetEnumerator() を独自に改良した。そして、LINQ を使い始めて 10 年になるが、ここにきて初めて IEnumerable<T> が内部でどのように動作しているのか理解した。

IEnumerable<T> は OnMemory でも RDB でも同じ記法で透過的に記述できる魔法だが、その名のとおり、シーケンシャルアクセスしか定義せず、ランダムアクセスを用意しない。n 番目の要素を取得するには .ElementAt(n) とするが、これは配列のインデクサアクセス array[n] とは異なり、.Skip(n).First() と同じ *1であり、実際に LINQ は .ElementAt(n) が呼ばれるたびに、.GetEnumerator() して列挙子を取得し、先頭から n 要素スキップして最初の要素を返す、という気の遠くなるような手順を踏む。

これをインデクサと同じくランダムアクセスとするようショートカットする (か否かを決める) のは、IEnumerable<T> インターフェースを持つデータ構造 *2 の実装の方であり、IEnumerable<T> インターフェース自体は我関せずという態度なのだ。また、いまのところ、C# コンパイラ .NET ランタイムも IEnumerable<T> やその実装の内部の挙動を最適化しない。

OnMemory データ操作も RDB データ操作も記述を可能な限り統一しようとしてきたが、実装の違いがある以上、実行効率とのトレードオフを勘案すると限界がある。LINQ は可読性を高めるが、やりすぎると却って可読性が落ちることも経験上わかっている。

LINQ の使いどころはやや控えめに考えることにし、C#LINQ 最適化チューニングするまでは脱 LINQ 至上原理主義を宣言しようと思う。

*1:LINQ to Entities で RDB アクセスするにはこの記法にする必要がある。

*2:固定長構造の配列やリストならランダムアクセスが可能だが、FileStream などの可変長構造ならランダムアクセスは不可能である。