Saturday, May 19, 2007

Using F# active patterns with LINQ expression trees

F# active patterns provide a nice way to handle complex object models.

In this post, I'm going to write a couple of active patterns to access parts of LINQ expression trees. As a little experiment I'm going to change the implementation of the Linq To Google Desktop expression tree handling from C# to F#.


The following definitions of active patterns provide access to sections of expression trees.


let (|BinaryExpression|_|) (x:Expression) =
if (x :? BinaryExpression)
then let be = (x :?> BinaryExpression)
in Some (be.Left,be.Right)
else None

let (|AndAlso|_|) (x:Expression) =
if (x.NodeType = ExpressionType.AndAlso)
then match x with
| BinaryExpression(l,r) -> Some (l,r)
| _ -> None
else None

let (|Equal|_|) (x:Expression) =
if (x.NodeType = ExpressionType.Equal)
then match x with
| BinaryExpression(l,r) -> Some (l,r)
| _ -> None
else None

let (|IsExpression|_|) (x:Expression) =
if (x :? TypeBinaryExpression)
then let be = (x :?> TypeBinaryExpression)
in Some (be.Expression,be.TypeOperand)
else None

let (|MethodCall|_|) (x:Expression) =
if (x.NodeType = ExpressionType.Call &&
(x :? MethodCallExpression))
then
let mc = x :?> MethodCallExpression
in Some (mc.Object,
mc.Method,
IEnumerable.to_list(mc.Arguments))
else None

let (|Cast|_|) (x:Expression) =
if (x.NodeType = ExpressionType.Convert
&& (x :? UnaryExpression))
then let ue = (x :?> UnaryExpression)
in Some (ue.Type,ue.Operand)
else None

let (|Lambda|_|) (x:Expression) =
if ( (x :? UnaryExpression) &&
((x :?> UnaryExpression).Operand :? LambdaExpression))
then let ue = (x :?> UnaryExpression)
in Some (ue.Operand :?> LambdaExpression).Body
else None

let (|MemberAccess|_|) (x:Expression) =
if (x.NodeType = ExpressionType.MemberAccess
&& (x :? MemberExpression))
then let ue = (x :?> MemberExpression)
in Some (ue.Expression,ue.Member)
else None

let (|MethodName|) (m:MethodInfo) = m.Name
let (|TypeName|) (t:Type) = t.Name

let (|PropertyWithName|_|) (m:MemberInfo) =
if (m :? PropertyInfo)
then Some (m.Name)
else None

let (|ExpressionType|) (x:Expression) = x.Type


As you can see, most of the active patterns are Partial Recognizers (see here), for example (|Equal|_|) . This is because of all the dynamic type tests that we need to do.


The C# code for expression tree handling presented on the Linq to Google Desktop post, is located in the GDesktop.CollectQueryInfo method. By using these new active pattern definitions the code could be rewritten in F# as follows:



let rec CollectQueryInfoFS (e:Expression, qi:GDQueryInfo) =
match e with
| Lambda(body) -> CollectQueryInfoFS (body,qi)
| AndAlso(l,r) -> CollectQueryInfoFS(l,qi);
CollectQueryInfoFS(r,qi)
| MethodCall(_,MethodName("Contains"),[argument]) ->
qi.AddTerm(GetStringFromArgument(argument))
| Equal(MemberAccess(ExpressionType(TypeName("GDFileResult")),
PropertyWithName("FileType")),
value) ->
qi.FileType <- GetStringFromArgument(value)
| Equal(MemberAccess(ExpressionType(TypeName("GDEmailResult")),
PropertyWithName(pName)),
value) ->
CollectEmailProperty(pName,GetStringFromArgument(value),qi)
| IsExpression(_,TypeName("GDFileResult")) ->
qi.ElementType <- new Nullable<GDElementType>( GDElementType.File )
| IsExpression(_,TypeName("GDEmailResult")) ->
qi.ElementType <- new Nullable<GDElementType>( GDElementType.Email )
| _ -> raise (new NotSupportedException(e.ToString()))


By using the active pattern definitions, the code is much smaller and easer to read and modify.

Right now only the expression tree handling part was translated to F#. The point where we call F# looks like this:


private GDQueryInfo ProcessWhereMethodCall(MethodCallExpression mcExpression)
{
Expression theObjectArgument = mcExpression.Arguments[0];
Expression whereExpressionTree = mcExpression.Arguments[1];

GDesktop provider =
(GDesktop)((ConstantExpression)theObjectArgument).Value;

GDQueryInfo qi = new GDQueryInfo();
//CollectQueryInfo(whereExpressionTree, qi);
Langexplr.FsharpTests.Langexplr.FsharpTests.CollectQueryInfoFS(
whereExpressionTree, qi);
return qi;

}


For future posts I'm going to try to rewrite the entire implementation.