Passing a generic function as a parameter

16,614

Solution 1

This answer doesn't explain the reason why, just how to work around the limitation.

Instead of passing an actual function, you can pass an object that has such a function:

interface IGenericFunc
{
    TResult Call<TArg,TResult>(TArg arg);
}

// ... in some class:

void Test(IGenericFunc genericFunc)
{
    // for example's sake only:
    int x = genericFunc.Call<String, int>("string");
    object y = genericFunc.Call<double, object>(2.3);
}

For your specific use case, it can be simplified to:

interface IDeserializerFunc
{
    T Call<T>(string arg);
}

// ... in some class:
void Test(IDeserializerFunc deserializer)
{
    int x = deserializer.Call<int>("3");
    double y = deserializer.Call<double>("3.2");
}

Solution 2

What you're asking to do isn't possible using generics alone. The compiler needs to generate two typed versions of your Transform function: one to return type A and one for type B. The compiler has no way of knowing to generate this at compile time; only by running the code would it know that A and B are required.

One way to solve it would be to pass in the two versions:

private void Execute()
{
    GeneralizedFunction("1", "2", i => Transform<A>(i), i => Transform<B>(i));
}

void GeneralizedFunction(string aStringA, string aStringB, Func<string, A> aAction,  Func<string, B> bAction)
{
    A result1 = aAction(aStringA);
    B result2 = bAction(aStringB);
}

The compiler knows exactly what it needs to generate in this case.

Solution 3

Try the following signature:

void GeneralizedFunction<T>(string aStringA, string aStringB, Func<string, T> aAction)

(Note that GeneralizedFunction has to be generic; the compiler will automatically guess the type parameter when calling the method).

Solution 4

It seems the answer is "no".

When you call Transform directly, you have to specify a type parameter:

int i = Transform<int>("");

So hypothetically, if you could pass an incompletely-constructed generic function like you want to, you'd need to specify the type parameters as well:

void GeneralizedFunction(string aStringA, string aStringB, Func<string, T> aAction)
{
    A result1 = aAction<A>(aStringA);
    B result2 = aAction<B>(aStringB);
    // Do something with A and B here
}

So it seems to me that you could hypothetically do this, if C# had a syntax like that.

But what's the use case? Aside from transforming strings to the default value of an arbitrary type, I don't see much use for this. How could you define a function that would provide a meaningful result in either of two different types using the same series of statements?

EDIT

An analysis of why it's not possible:

When you use a lambda expression in your code, it is compiled into either a delegate or an expression tree; in this case, it's a delegate. You can't have an instance of an "open" generic type; in other words, to create an object from a generic type, all type parameters must be specified. In other words, there's no way to have an instance of a delegate without providing arguments for all of its type parameters.

One of the C# compiler's helpful features is implicit method group conversions, where the name of a method (a "method group") can be implicitly converted to a delegate type representing one of the overloads of that method. Similarly, the compiler implicitly converts a lambda expression to a delegate type. In both cases, the compiler emits code to create an instance of the delegate type (in this case, to pass it to the function). But the instance of that delegate type still needs to have a type argument for each of its type parameters.

To pass the generic function as a generic function, it seems, the compiler would need to be able to pass the method group or the lambda expression to the method without conversion, so the aAction parameter would somehow have a type of "method group" or "lambda expression." Then, the implicit conversion to a delegate type could happen at the call sites A result1 = aAction<A>(aStringA); and B result2 = aAction<B>(aStringB);. Of course, at this point, we are well into the universe of contrafactuals and hypotheticals.

The solution I came up with over lunch was this, assuming a function Deserialize<T> that takes a string containing serialized data and returns an object of type T:

void GeneralizedFunction<T>(string aStringA, string aStringB, Func<T, string> stringGetter)
{
    A result1 = Deserialize<A>(stringGetter(aStringA));
    B result2 = Deserialize<B>(stringGetter(aStringB));
}

void Example(string serializedA, string serializedB, string pathToA, string pathToB, FileInfo a, FileInfo b)
{
    GeneralizedFunction(serializedA, serializedB, s => s);
    GeneralizedFunction(pathToA, pathToB, File.ReadAllText);
    GeneralizedFunction(a, b, fi => File.ReadAllText(fi.FullName));
}
Share:
16,614

Related videos on Youtube

Max
Author by

Max

Updated on June 04, 2022

Comments

  • Max
    Max almost 2 years

    I know that what I'm doing can be done in a different way, but I'm curious about how things work. The following is a simplified code which doesn't compile, but it supposed to show my goal.

    private void Execute()
    {
        GeneralizedFunction("1", "2", i => Transform(i));
    }
    
    void GeneralizedFunction(string aStringA, string aStringB, Func<string, T> aAction)
    {
        A result1 = aAction(aStringA);
        B result2 = aAction(aStringB);
        // Do something with A and B here
    }
    
    T Transform<T>(string aString)
    {
        return default(T);
    }
    

    Transform is a generic convertion from string to some object (think deserialization). GeneralizedFunction uses two specializations of transform: one for type A and one for type B. I know I can do this in a number of other ways (say by introducing a parameter for the type of the object), but I'm looking for explanations of whether it is possible or impossible to do this with generics/lambdas. If Transform is specialized before it is passed as a parameter to GeneralizedFunction, then it's impossible. Then the question is why this possibility is restricted.

  • Max
    Max about 12 years
    I did try it. The problem is you are trying to refer to both types A and B with T here. It was my intention to remove <T> from function declaration.
  • Matthias
    Matthias about 12 years
    Then you'll have to replace both A and B with T.
  • Max
    Max about 12 years
    I need to perform specific operations on the objects inside GeneralizedFunction for simplicity. I haven't written any details in my sample code, but everything that is written is necessary.
  • Max
    Max about 12 years
    Yes I know that I could pass two instances, but I hoped that it is possible to pass a GENERIC function (hence the title of my question) and create two specializations of that generic function inside the GeneralizedFunction.
  • Matthias
    Matthias about 12 years
    I think I now understand what you want: "void GeneralizedFunction(string aStringA, string aStringB, Func<string, A> aAction, Func<string, B> bAction)". However, you will have to pass the method twice.
  • Tim Rogers
    Tim Rogers about 12 years
    I know the code isn't what you want. Your question was why the possibility is restricted. Hopefully you understand now why what you want to do won't work with the compiler.
  • Max
    Max about 12 years
    If I look at the IL code generated for Transform function it seems that there's only one version of it. Even when I apply it to two classes of objects. So it seems that the specialization of a generic function is done at runtime. Isn't it?
  • phoog
    phoog about 12 years
    @Max The specialization of the function is done by the just-in-time compiler, so yes, that's done at run time. However, the JIT compiler creates only one specialized version of the function for all reference types. The C# compiler guarantees that this can be done with type safety by virtue of its compile-time type analysis, so run-time type checks are not needed. You're probably aware that you could achieve what you want with reflection, but the solution would be messier than just passing the function twice.
  • Max
    Max about 12 years
    @phoog Does it meant that hypothetically this could be implemented if type constraints are specified (so A and B are guaranteed to be reference types)?
  • phoog
    phoog about 12 years
    @Max, hmm, perhaps, but that also means that you have to declare both type parameters, in order to constrain them to be reference types.
  • Max
    Max about 12 years
    One use case is deserialization. The string is a representation of an object and Transform creates an instance of that object from its string representation
  • phoog
    phoog about 12 years
    @Max But what's the use case for passing Transform to GeneralizedFunction rather than just calling it directly? Anyway, wouldn't a generic T Transform<T>(string) just be a convenience method around object Deserialize(type, string) like T Transform<T>(string s) { return (T)Deserialize(typeof(T), s); }
  • Max
    Max about 12 years
    For instance Transform1 might convert a string to an object, Transform2 might convert a file which a string points to to an object
  • phoog
    phoog about 12 years
    @Max, ah, ok, I understand. Will think on it over lunch :)
  • Max
    Max about 12 years
    The problem is not the implementation. There are many ways to do this. I was just curious about whether it was possible to do it in a particular way in C#. And why it isn't possible.
  • phoog
    phoog about 12 years
    @Max Why it isn't possible: see edited answer. I also included my solution from lunch, in case you hand't already thought of that approach.
  • sinelaw
    sinelaw almost 5 years
    Technically, the requested feature is rank-2 parametric polymorphism. C# (and Java, etc.) support only rank-1.