Is there no way for Csharp to write a truly static λ expression?

175 views Asked by At

Language : C# .Net Version : 8.0 IDE : VS2020 / VSCode OS : Windows11

Is there no way for Csharp to write a truly static λ expression?

I studied currying today and wrote a function :

using System.Linq.Expressions;
using Curryfy;

object Apply(Delegate _operator, object? arg1)
{
    Type t = _operator.GetType();
    if (!t.IsGenericType || t.GenericTypeArguments.Length < 2 || t.Name != "Func`" + t.GenericTypeArguments.Length)
        throw new Exception();

    var args = _operator.Method.GetParameters().Select(p => Expression.Variable(p.ParameterType)).ToArray();
    Expression expression = Expression.Call(_operator.Method, args);
    foreach (var arg in args.Reverse())
        expression = *Expression.Lambda(expression, arg);*
    Console.WriteLine(expression);
    return null;
}
***int sum(int a, int b) => a + b;***
System.Console.WriteLine(sum);
Apply(sum, 10);

It can run correctly and the results are as I expected :

System.Func`3[System.Int32,System.Int32,System.Int32]
Param_0 => Param_1 => <<Main>$>g__sum|0_1(Param_0, Param_1)

This sum function doesn't even use 'static'. But when I change this line to :

Func<int, int, int> sum = static (int a, int b) => a + b;

The Expression.Call(_operator.Method, args) throw a exception said :

Static method requires null instance, non-static method requires non-null instance.

What is the essential difference between these two ways of writing?

update at 14:34 1/11/2024:

Inspired by Michael Liu, I changed these implementation of sum to :

var a = Expression.Variable(typeof(int));
var b = Expression.Variable(typeof(int));
var aplusb = Expression.Add(a, b);
var sumExpression = Expression.Lambda(aplusb, a, b);
var sum = sumExpression.Compile();

Then Expression.Call(_operator.Method, args) didn't throw any exception.

3

There are 3 answers

0
Teodor Mihail On

You can use a Func<T in, T out> to store the lambda expression statically. T in is the data type used for the input and T out is the data type for the output.

static Func<int, int> func = x => x * x;

//

//

Console.WriteLine(func.Invoke(2));

You can invoke the result synchrounously using the Invoke method and pass the input to it.

If you want to call it asynchronously, .NET Core is not supporting the BeginInvoke and EndInvoke calls, so you will have to call the Invoke method in a Task<T> method in order to call it asynchronously.

namespace Test
{
    class Program
    {
        static Func<int, int> func = x => x * x;
        public static void Main()
        {
            AsyncTaskProcess();
        }

        public static async void AsyncTaskProcess()
        {
            int result = await FuncExe(func, 2);
            Console.WriteLine(result);
        }

        public static Task<int> FuncExe(Func<int, int> func, int param){
            return Task.FromResult(func.Invoke(param));
        }
    }
}

3
Michael Liu On

What is the essential difference between these two ways of writing?

It looks like this difference in behavior is the result of a choice made by the current implementation of the C# compiler. When you write

int sum(int a, int b) => a + b;

the compiler generates a static method (even though you didn't mark it static), which works with the Expression.Call(MethodInfo, Expression[]) overload that you're using.

But when you write

Func<int, int, int> sum = static (int a, int b) => a + b;

the compiler actually generates an instance method, even though you explicitly marked it static! Apparently, this is for performance reasons (calling a static method via a delegate requires some extra argument shuffling compared to calling an instance method).

You can work around this behavior by updating your code to handle instance methods:

Expression expression = _operator.Method.IsStatic
    ? Expression.Call(_operator.Method, args)
    : Expression.Call(Expression.New(_operator.Method.DeclaringType), _operator.Method, args);

This will instantiate an instance of the compiler-generated class that holds the actual implementation of your sum method.

Caution: We're depending on some implementation details of the C# compiler here, which are subject to change. If you want to write code that's guaranteed to work forever, I recommend not passing lambda expressions to Apply.

0
Guru Stron On

But when I change this line to : Func<int, int, int> sum = static (int a, int b) => a + b;

You don't need static here, you will have the same issue with just:

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

The thing here is that you are declaring an anonymous function (lamdba) which is a kind of syntactic sugar which is handled by compiler and actually is expanded into something like the following (see yourself @sharplab):

[Serializable]
[CompilerGenerated]
private sealed class TheGeneratedClass
{
    public static readonly TheGeneratedClass <>9 = new TheGeneratedClass();

    public static Func<int, int, int> <>9__0_0;

    internal int TheGeneratedMethodName(int a, int b)
    {
        return a + b;
    }
}

As you can see that the generated method (internal int TheGeneratedMethodName(int a, int b)) is actually an instance one which requires instance, i.e. something like the following - new TheGeneratedClass().TheGeneratedMethodName. I.e. in the case of anonymous lambda the expression should create an instance of the holding type. For example:

Expression expression = Expression.Call(
    Expression.New(_operator.Method.DeclaringType), 
    _operator.Method, 
    args);