うならぼ

どうも。

クエリ式にないLINQのメソッドをクエリ式の中で呼び出すハック

最近になってクエリ式も使うようになりました。ただ惜しいのが、クエリ式に続けてIEnumerableの拡張メソッドを呼び出すのが面倒なこと。

var sum = (
    from x in Enumerable.Range(1, 10)
    select x * 2
).Sum();

括弧が必要なのもそうですし、これどうインデントしたものかと。 IEnumerable<T> の状態で変数に入れておけばいいんですが、LINQPadのExpressionモードとかでメソッドチェインで全部済ませようと思うとちょっと・・・ね?

というわけで拡張メソッドで闇の拡張を作ったというメモ。

どうするのか

結局のところ各メソッドの引数・戻り値の型すらも規定はなく、機械的にメソッド呼び出しに変換したコードが動けばそれでいいみたいで。Enumerable.Select に渡したデリゲートはコレクションの要素を受け取るわけですが、コレクションそのものを受け取るような Select メソッドを書いてもいいのです。というのが今回のアプローチ。

ただ新規実装した拡張メソッドを優先させるために、フックしたいところで型を変えています。あとfromやletが重なると識別子をまとめた匿名型が自動的に生成されますが、これに合わせて式内の識別子がメンバ参照に書き換えられてしまうので、IEnumerable<T> のまま渡そうとしたり、sourceを勝手にいじろうとするときに問題になります。これは正直どうしようもなさそうなので、select .. into で一度まとめてもらうことにしました。

実装A

static class Unwrap
{
    public delegate TResult Unwrapper<T, TResult>(T wrapped);
    public static TResult Select<T, TResult>(this T source, Func<T, Unwrapper<T, TResult>> unwrap)
        => unwrap(source)(source);
    public static Unwrapper<T, TResult> With<T, TResult>(T wrapped, Unwrapper<T, TResult> func) => func;
}

void Main()
{
    var v =
        from x in Enumerable.Range(1,10)
        let y = x * 2
        select y into _
        select Unwrap.With(_, s => s.Sum());
    v.Dump();
}

デリゲートを返すデリゲートをSelectに渡すと、それをsourceに適用する、という形。誤発動を防ぐためにあえて独自のデリゲート型。

ヘルパ関数でsourceも引数に取ることで、第二パラメータの引数の型を推論させた感じ。少なくともLINQPadではこれで推論できる。

実装B

static class UnwrapExtensions
{
    public static Unwrap<T> ToUnwrap<T>(this T obj) { return new Unwrap<T>(obj); }
    public static TResult Select<T, TResult>(this T source, Func<T, Unwrap<TResult>> selector)
        => selector(source).Value;
    public struct Unwrap<T>
    {
        public T Value { get; }
        public Unwrap(T obj) { Value = obj; }
    }
}

void Main()
{
    var v =
        from x in Enumerable.Range(1, 10)
        let y = x * 2
        select y into _
        select _.Sum().ToUnwrap();
    v.Dump();
}

デリゲートを重ねることはせず、結果の型を変えておいてSelectはそれを取り出すだけ。

ToUnwrap() を呼ぶかどうかで _ の型が変わるのはやっぱり変な感じですね。

おまけ:ステートメントブロック代わりのクエリ式

static class IdentityQuery
{
    public static Identity<bool> Begin() => new Identity<bool>();
    public static Identity<T> End<T>(T obj) => new Identity<T>(obj);
    
    public struct Identity<T>
    {
        private T Value;
        
        public Identity(T val) { Value = val; }

        public Identity<TResult> Select<TResult>(Func<T, TResult> selector)
            => new Identity<TResult>(selector(this.Value));
        public TResult Select<TResult>(Func<T, Identity<TResult>> selector)
            => selector(this.Value).Value;
    }
}

void Main()
{
    var v =
        from _ in IdentityQuery.Begin()
        let x = 1
        let y = 2
        let z = x + y
        select IdentityQuery.End(z);
    v.Dump();
}

名前はなんか違う気もする。

複数fromの対応を切ったらSelectManyを捨てれたのでだいぶすっきり。