C# 4 expressions: variables [Part II]
Reading the first part is mandatory !
The goal is still the same: enhance the C# 4 expression capabilities.
Now we have block support, let’s find a way to add variables. We can not really ‘code’ something without them…
In our first sample, the main lambda already had a parameter and we were able to use it inside our expression. Technically, adding variables is just creating new local parameters which is easy:
Expression.Parameter(typeof(int), “param”) then we can add this parameter as a variable of a block.
Once again, what is hard is to find a way to express and recognize them easily.
First idea, creating :
Block.DefineVariable(typeof(int), “param”)
Block.ReadVariable(“param”)
Block.AssignVariable(“param”, 1);
Easy to do and easy to transform, except it’s long to write and non typed. We would need a lot of cast instructions to make it run for real, heavy, no test at compile time, unsafe…not good.
Here is what I propose. As lambda can have access to the scope of their hosting method (see this article), we could imagine to create our needed variables outside the expression and then convert them to local parameter during the transformation.
Quite a strange solution with unnecessary declarations inside the hosting method but the idea is here. Let’s create an intermediate scope between the lambda and its hosting method.
public class Block
{
public static Block Start<T>(T locals, Action<T> block)
{
throw new NotImplementedException();
}
...
Imagine a Start method taking a first instance (that resolves the generic parameter T) and then a second argument defining an action with this a parameter of this same type.
Now imagine we use an anonymous type to define this first instance.
Expression<Action<int>> exp = i =>
Block.Start(new { j = 1 }, b =>
Console.WriteLine(b.j)
);
In this code, type of b is defined by ‘new { j = 1 }’.
b is of course accessible in the body of the lambda and so are all its properties.
b.j is accessible and typed correctly.
The only default is b.j is read-only because such are anonymous types properties. But whatever, C# does not supports affectation in expressions. So we will symbolically write something like :
Block.Assign(() => b.j, 1);
…and we will transform it into something executable.
Let’s talk about the transformation. We need to recognize Block.Start, analyze the type of the first argument, create as many local parameters as the first argument exposes properties (same name, same type of course) and then replace all the references to b.X by the corresponding parameter. Last feature, we can initialize all the parameters with the values found in the anonymous type definition.
We also have to make it run correctly recursively.
private Expression VisitBlockMethodCall(MethodCallExpression node)
{
if (IsMethodOf<Block>(node, "Assign"))
{
var left = node.Arguments[0]
as LambdaExpression;
return Expression.Assign(Visit(left.Body
,Visit(node.Arguments[1]));
}
if (IsMethodOf<Block>(node, "Start", true))
{
//Anonymous type analyzed to generate
//parameters
var newExpression = node.Arguments[0]
as NewExpression;
var variablesToAdd =
newExpression.Type.GetProperties()
.Select((p, i) =>
new {
Parameter = Expression.Parameter(
p.PropertyType,
p.Name),
Value = newExpression.Arguments[i]
}).ToArray();
//Prepares properties to be replaced
//by parameters and adds them to a
//global collection
var blockStartLambda = node.Arguments[1]
as LambdaExpression;
var parameters =
variablesToAdd.ToDictionary(
v => v.Parameter.Name,
v => v.Parameter);
blockStartParameters.Add(
blockStartLambda.Parameters[0],
parameters);
//VisitMember will make the right
//replacements during this recursive
//call
var body = Visit(blockStartLambda.Body);
//Removes from collection
blockStartParameters.Remove(
blockStartLambda.Parameters[0]);
//Creates a block with parameters
//declaration and initialization
var blockWithVariablesInitialization =
variablesToAdd.Select(v =>
Expression.Assign(v.Parameter, v.Value))
.Concat(new Expression[] { body });
var block = Expression.Block(
variablesToAdd.Select(v => v.Parameter),
blockWithVariablesInitialization);
return block;
}
...
The following code is using the prepared references of our anonymous type and replaces them in the VisitMember() method.
protected override Expression VisitMember(MemberExpression node)
{
if ((node.Member.MemberType
== MemberTypes.Property)
&& (node.Expression is ParameterExpression))
{
Dictionary<string, ParameterExpression>
parameters = null;
if (blockStartParameters.TryGetValue(
node.Expression as ParameterExpression,
out parameters))
return parameters[node.Member.Name];
}
return base.VisitMember(node);
}
Now let’s see the result of this transformation.
Expression<Action<int>> exp = i =>
Block.Start(new { j = 1 }, b =>
Console.WriteLine(b.j)
);
generates this expression :
.Lambda #Lambda1<System.Action`1[System.Int32]>(System.Int32 $i) {
.Call CSharp4Expressions.Block.Start(
.New <>f__AnonymousType0`1[System.Int32](1),
.Lambda #Lambda2<System.Action`1[<>f__AnonymousType0`1[System.Int32]]>)
}
.Lambda #Lambda2<System.Action`1[<>f__AnonymousType0`1[System.Int32]]>(<>f__AnonymousType0`1[System.Int32] $b) {
.Call System.Console.WriteLine($b.j)
}
which is transformed into :
.Lambda #Lambda1<System.Action`1[System.Int32]>(System.Int32 $i) {
.Block() {
.Block(System.Int32 $j) {
$j = 1;
.Call System.Console.WriteLine($j)
}
}
}
We finally compile it and it runs correctly
Let’s test if we can nest declarations (variables local to child blocks).
Expression<Action<int>> exp = i =>
Block.Start(new { j = 1 }, b =>
Block.Start(new { i = 2 }, b2 =>
Console.WriteLine(b.j + b2.i)));
Ok we now have block support and a way to declare variables.
If reading is natural, we need to be able to assign values to those variables.
It’s a quite easy transformation. We first create an Assign() method in the Block class.
public class Block
{
public Block Assign<T>(Func<T> leftExpression, T rightExpression)
{
throw new NotImplementedException();
}
...
and then we catch it in our transformation engine replacing it with an Expression.Assign()
private Expression VisitBlockMethodCall(MethodCallExpression node)
{
if (IsMethodOf<Block>(node, "Assign"))
{
var left = node.Arguments[0] as
LambdaExpression;
return Expression.Assign(
Visit(left.Body),
Visit(node.Arguments[1]));
}
...
Let’s try it
Expression<Action<int>> exp = i =>
Block.Start(new { j = 1 }, b =>
Block.Default
._(() => Console.WriteLine(b.j))
.Assign(() => b.j, 2)
._(() => Console.WriteLine(b.j))
);
and we correctly get
Enough for this post !
Next article will add Label, Goto, Loop, IfThenElse, While and For instructions.
The whole project is available here : https://code.msdn.microsoft.com/CSharp4Expressions/
Comments
- Anonymous
March 16, 2010
The comment has been removed - Anonymous
May 09, 2010
The comment has been removed