アプリケーション式

こんにちは



最近、他の誰かのコードを調べてみたところ、IQueryableツリーとExpessionツリーに関するかなり興味深い問題に出会いました。 このソリューションが誰かに役立つことを願っています。



タスクは、あるExpressionを別のExpression内で再利用することです。たとえば、いくつかのfがあります。



Expression<Func<int, int, int>> f = (a, b) => a + b;
      
      







そして、次のように、他の式の中でこのfを使用したいと思います。



 Expression<Func<int, int, int, int>> g = (a, b, c) => f(a+b,b)*c;
      
      







さらに、結果の表現は「純粋」である必要があります。 IQueryable内での使用に適しています(コンパイルされた関数などなし)







これらの2行をコンパイルしようとすると、gの定義が間違っていることがわかります「f」は「変数」ですが、「メソッド」のように使用されます。一般に、fは式ツリーのルートであり、関数またはファンクターでない場合。 次のように書くことができます:

 Expression<Func<int, int, int, int>> g = (a, b, c) => f.Compile()(a+b,b)*c;
      
      







しかし、その後、式は最終的に次のようになります。



(a, b, c) => (Invoke(value(ExpressionReducer.Program+<>c__DisplayClass0).f.Compile(), (a + b), b) * c)







当然、IQueryableはこれを理解しません。



最も簡単で明白なアイデアは、単純にfをその本体自体に置き換えることです。おおまかに言うと、fの用語をgに「適用」することです(正直、私はラムダ消失にまったく強くありませんが、私の意見では適用です)。



このような「アプリケーション」の場合、gの式ツリーを少し書き直す必要があります。具体的には、Invoke(Compile())呼び出しを関数fの本体で置き換え、本体fでそのパラメーターをInvoke引数の値で置き換えます。



 (a, b, c) => f.Compile()(a+b,b)*c
      
      





取得する

 (a, b, c) => ((a+b)+b)*c
      
      







開始するには、かさばるInvoke(コンパイル)を取り除き、そのような拡張メソッドで置き換えましょう。



 public static T Apply<T,T1,T2>(this Expression<Func<T1,T2,T>> expression, T1 t1, T2 t2) { return expression.Compile()(t1, t2); } //... Expression<Func<int, int, int, int>> g = (a, b, c) => f.Apply(a + b, b) * c;
      
      







実際、Apply関数の本体は重要ではありません。変換中に呼び出されることはありませんが、単純化せずにgを使用する場合は有効な本体を使用すると便利です。



結果のツリーを詳しく見てみましょう。





実際には、次の手順を実行します。

  1. Applyメソッドの呼び出しを見つけます。
  2. Apply関数の最初の引数からラムダ関数fを取得します。
  3. ラムダ本体の引数を、Apply関数の残りのパラメーターに置き換えます。
  4. .Callツリーでbody fに置き換えます。




最初の項目は簡単に実行できます-System.Linq.Expressions名前空間のExpressionVisitorクラスを使用します。 これは、式ツリーのすべてのノードにアクセスするだけでなく、その一部を書き換えることができる非常に便利なクラスです(詳細については、 http://msdn.microsoft.com/en-us/library/bb546136%28v=vs.90%29.aspxを参照してください )ApplyメソッドがExpressionReductorクラスにあると仮定します。

 private class InvokerVisitor : ExpressionVisitor { protected override Expression VisitMethodCall(MethodCallExpression node) { if (node.Method.DeclaringType == typeof (ExpressionReductor) && node.Method.Name == "Apply") { //     } return base.VisitMethodCall(node); } }
      
      





2番目のポイントはやや複雑です。 ツリーからわかるように、fは自動生成されたクラスExpressionReducer.Program + <> c__DisplayClass0のフィールドになりました。これは、メソッドの本体で宣言された、またはメソッドパラメーターとして来るすべてのファンクターまたは式でC#が動作する方法です。 他のオプションのうち、これは名前付きクラスのフィールドまたはプロパティ、または関数呼び出しの結果です。

簡単にするために、最初のケースのみを考慮します(残りも同様に実装できます):fは特定のクラスのフィールドです。

 class FieldLambdaFinder : ExpressionVisitor { protected override Expression VisitMember(MemberExpression node) { var constantExpression = (ConstantExpression) node.Expression; var info = (FieldInfo) node.Member; var fieldValue = (Expression)info.GetValue(constantExpression.Value); return fieldValue; } public Expression Find(Expression expression) { return Visit(expression); } }
      
      





3番目の項目は非常に単純です-辞書を作成して(パラメーターf->パラメーターの適用)、fの本体のすべてのParameterExpressionを置き換えましょう:

 internal class Replacer : ExpressionVisitor { private Dictionary<ParameterExpression, Expression> _replacements; public Replacer(IEnumerable<ParameterExpression> what, IEnumerable<Expression> with) { _replacements = what.Zip(with, (param, expr) => new { param, expr }).ToDictionary(x => x.param, x => x.expr); } public Expression Replace(Expression body) { return Visit(body); } protected override Expression VisitParameter(ParameterExpression node) { Expression replacement; return _replacements.TryGetValue(node, out replacement) ? replacement : base.VisitParameter(node); } }
      
      







最後の項目には、アセンブリのすべてが表示されます。

 private class InvokerVisitor : ExpressionVisitor { protected override Expression VisitMethodCall(MethodCallExpression node) { if (node.Method.DeclaringType == typeof (ExpressionReductor) && node.Method.Name == "Apply") { var lambda = GetLambda(node.Arguments[0]); return Replace(lambda, node.Arguments.Skip(1)); } return base.VisitMethodCall(node); } private Expression Replace(LambdaExpression lambda, IEnumerable<Expression> arguments) { var replacer = new Replacer(lambda.Parameters, arguments); return replacer.Replace(lambda.Body); } private LambdaExpression GetLambda(Expression expression) { var finder = new FieldLambdaFinder(); return (LambdaExpression) finder.Find(expression); } }
      
      







メソッド自体を単純化する:

 public static Expression<T> Simplify<T>(this Expression<T> expression) { var invoker = new InvokerVisitor(); return (Expression<T>) invoker.Visit(expression); }
      
      







すぐにすべてを見つけることができます



その結果、必要なものが得られました。

 Expression<Func<int, int, int>> f = (a, b) => a + b; Expression<Func<int, int, int, int>> g = (a, b, c) => f.Apply(a + b, b)*c; g = g.Simplify();
      
      







残りの問題:

  1. 他の場合にfを取得する方法。
  2. Applyパラメーターが副作用を持つ他の関数の呼び出しである場合、置換は正しくありません。 私たちの場合、IQueryableで操作しているため、これはできませんが、これを覚えておく必要があります。
  3. Simplify関数は計算を折りたたみません。f.Apply(5、5)は、(10)ではなく(5 + 5)に簡略化します。
  4. Simplify関数は再帰的ではありません。つまり、f.Apply(a、f.Apply(b、c))などの例では、何度も呼び出す必要があります。



All Articles