Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing lambda functions as named parameters in C#

Compile this simple program:

class Program
{
    static void Foo( Action bar )
    {
        bar();
    }

    static void Main( string[] args )
    {
        Foo( () => Console.WriteLine( "42" ) );
    }
}

Nothing strange there. If we make an error in the lambda function body:

Foo( () => Console.LineWrite( "42" ) );

the compiler returns an error message:

error CS0117: 'System.Console' does not contain a definition for 'LineWrite'

So far so good. Now, let's use a named parameter in the call to Foo:

Foo( bar: () => Console.LineWrite( "42" ) );

This time, the compiler messages are somewhat confusing:

error CS1502: The best overloaded method match for 
              'CA.Program.Foo(System.Action)' has some invalid arguments 
error CS1503: Argument 1: cannot convert from 'lambda expression' to 'System.Action'

What's going on? Why doesn't it report the actual error?

Note that we do get the correct error message if we use an anonymous method instead of the lambda:

Foo( bar: delegate { Console.LineWrite( "42" ); } );
like image 350
Danko Durbić Avatar asked Nov 08 '11 16:11

Danko Durbić


People also ask

Does C support lambda function?

Modern C++ has lambda expressions. However, in C you have to define a function by name and pass a pointer — not a huge problem, but it can get messy if you have a lot of callback functions that you use only one time. It's just hard to think up that many disposable function names.

Can lambda functions take parameters?

A lambda function can have any number of parameters, but the function body can only contain one expression.

Can we write Parameterless lambda expression?

No, there isn't. Lambda expressions are optimised (in terms of syntax) for the single parameter case. I know that the C# team feels your pain, and have tried to find an alternative. Whether there ever will be one or not is a different matter.


2 Answers

Why doesn't it report the actual error?

No, that's the problem; it is reporting the actual error.

Let me explain with a slightly more complicated example. Suppose you have this:

class CustomerCollection
{
    public IEnumerable<R> Select<R>(Func<Customer, R> projection) {...}
}
....
customers.Select( (Customer c)=>c.FristNmae );

OK, what is the error according to the C# specification? You have to read the specification very carefully here. Let's work it out.

  • We have a call to Select as a function call with a single argument and no type arguments. We do a lookup on Select in CustomerCollection, searching for invocable things named Select -- that is, things like fields of delegate type, or methods. Since we have no type arguments specified, we match on any generic method Select. We find one and build a method group out of it. The method group contains a single element.

  • The method group now must be analyzed by overload resolution to first determine the candidate set, and then from that determine the applicable candidate set, and from that determine the best applicable candidate, and from that determine the finally validated best applicable candidate. If any of those operations fail then overload resolution must fail with an error. Which one of them fails?

  • We start by building the candidate set. In order to get a candidate we must perform method type inference to determine the value of type argument R. How does method type inference work?

  • We have a lambda whose parameter types are all known -- the formal parameter is Customer. In order to determine R, we must make a mapping from the return type of the lambda to R. What is the return type of the lambda?

  • We assume that c is Customer and attempt to analyze the lambda body. Doing so does a lookup of FristNmae in the context of Customer, and the lookup fails.

  • Therefore, lambda return type inference fails and no bound is added to R.

  • After all the arguments are analyzed there are no bounds on R. Method type inference is therefore unable to determine a type for R.

  • Therefore method type inference fails.

  • Therefore no method is added to the candidate set.

  • Therefore, the candidate set is empty.

  • Therefore there can be no applicable candidates.

  • Therefore, the correct error message here would be something like "overload resolution was unable to find a finally-validated best applicable candidate because the candidate set was empty."

Customers would be very unhappy with that error message. We have built a considerable number of heuristics into the error reporting algorith that attempts to deduce the more "fundamental" error that the user could actually take action on to fix the error. We reason:

  • The actual error is that the candidate set was empty. Why was the candidate set empty?

  • Because there was only one method in the method group and type inference failed.

OK, should we report the error "overload resolution failed because method type inference failed"? Again, customers would be unhappy with that. Instead we again ask the question "why did method type inference fail?"

  • Because the bound set of R was empty.

That's a lousy error too. Why was the bounds set empty?

  • Because the only argument from which we could determine R was a lambda's whose return type could not be inferred.

OK, should we report the error "overload resolution failed because lambda return type inference failed to infer a return type"? Again, customers would be unhappy with that. Instead we ask the question "why did the lambda fail to infer a return type?"

  • Because Customer does not have a member named FristNmae.

And that is the error we actually report.

So you see the absolutely tortuous chain of reasoning we have to go through in order to give the error message that you want. We can't just say what went wrong -- that overload resolution was given an empty candidate set -- we have to dig back into the past to determine how overload resolution got into that state.

The code that does so is exceedingly complex; it deals with more complicated situations than the one I just presented, including cases where there are n different generic methods and type inference fails for m different reasons and we have to work out from among all of them what is the "best" reason to give the user. Recall that in reality there are a dozen different kinds of Select and overload resolution on all of them might fail for different reasons or the same reason.

There are heuristics in the error reporting of the compiler for dealing with all kinds of overload resolution failures; the one I described is just one of them.

So now let's look at your particular case. What is the real error?

  • We have a method group with a single method in it, Foo. Can we build a candidate set?

  • Yes. There is a candidate. The method Foo is a candidate for the call because it has every required parameter supplied -- bar -- and no extra parameters.

  • OK, the candidate set has a single method in it. Is there an applicable member of the candidate set?

  • No. The argument corresponding to bar cannot be converted to the formal parameter type because the lambda body contains an error.

  • Therefore the applicable candidate set is empty, and therefore there is no finally validated best applicable candidate, and therefore overload resolution fails.

So what should the error be? Again, we can't just say "overload resolution failed to find a finally validated best applicable candidate" because customers would hate us. We have to start digging for the error message. Why did overload resolution fail?

  • Because the applicable candidate set was empty.

Why was it empty?

  • Because every candidate in it was rejected.

Was there a best possible candidate?

  • Yes, there was only one candidate.

Why was it rejected?

  • Because its argument was not convertible to the formal parameter type.

OK, at this point apparently the heuristic that handles overload resolution problems that involve named arguments decides that we've dug far enough and that this is the error we should report. If we do not have named arguments then some other heuristic asks:

Why was the argument not convertible?

  • Because the lambda body contained an error.

And we then report that error.

The error heuristics are not perfect; far from it. Coincidentally I am this week doing a heavy rearchitecture of the "simple" overload resolution error reporting heuristics -- just stuff like when to say "there wasn't a method that took 2 parameters" and when to say "the method you want is private" and when to say "there's no parameter that corresponds to that name", and so on; it is entirely possible that you are calling a method with two arguments, there are no public methods of that name with two parameters, there is one that is private but one of them has a named argument that does not match. Quick, what error should we report? We have to make a best guess, and sometimes there is a better guess that we could have made but were not sophisticated enough to make.

Even getting that right is proving to be a very tricky job. When we eventually get to rearchitecting the big heavy duty heuristics -- like how to deal with failures of method type inference inside of LINQ expressions -- I'll revisit your case and see if we can improve the heuristic.

But since the error message you are getting is completely correct, this is not a bug in the compiler; rather, it is merely a shortcoming of the error reporting heuristic in a particular case.

like image 51
Eric Lippert Avatar answered Oct 11 '22 21:10

Eric Lippert


EDIT: Eric Lippert's answer describes (much better) the issue - please see his answer for the 'real deal'

FINAL EDIT: As unflattering as it is for one to leave a public demonstration of their own ignorance in the wild, there's no gain in veiling ignorance behind a push of the delete button. Hopefully someone else can benefit from my quixotic answer :)

Thanks Eric Lippert and svick for being patient and kindly correcting my flawed understanding!


The reason that you are getting the 'wrong' error message here is because of variance and compiler-inference of types combined with how the compiler handles type resolution of named parameters

The type of the prime example () => Console.LineWrite( "42" )

Through the magic of type inference and covariance, this has the same end result as

Foo( bar: delegate { Console.LineWrite( "42" ); } );

The first block could be either of type LambdaExpression or delegate; which it is depends on usage and inference.

Given that, is it no wonder that the compiler gets confused when you pass it a parameter that's supposed to be an Action but which could be a covariant object of a different type? The error message is the main key that points toward type resolution being the issue.

Let's look at the IL for further clues: All of the examples given compile to this in LINQPad:

IL_0000:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0005:  brtrue.s    IL_0018
IL_0007:  ldnull      
IL_0008:  ldftn       UserQuery.<Main>b__0
IL_000E:  newobj      System.Action..ctor
IL_0013:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0018:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_001D:  call        UserQuery.Foo

Foo:
IL_0000:  ldarg.0     
**IL_0001:  callvirt    System.Action.Invoke**
IL_0006:  ret         

<Main>b__0:
IL_0000:  ldstr       "42"
IL_0005:  call        System.Console.WriteLine
IL_000A:  ret

Note the ** around the call to System.Action.Invoke: callvirt is exactly what it seems like: a virtual method call.

When you call Foo with a named argument, you're telling the compiler that you're passing an Action, when what you're really passing is a LambdaExpression. Normally, this is compiled (note the CachedAnonymousMethodDelegate1 in the IL called after the ctor for Action) to an Action, but since you explicitly told the compiler you were passing an action, it attempts to use the LambdaExpression passed in as an Action, instead of treating it as an expression!

Short: named parameter resolution fails because of the error in the lambda expression (which is a hard failure in and of itself)

Here's the other tell:

Action b = () => Console.LineWrite("42");
Foo(bar: b);

yields the expected error message.

I'm probably not 100% accurate on some of the IL stuff, but I hope I conveyed the general idea

EDIT: dlev made a great point in the comments of the OP about the order of overload resolution also playing a part.

like image 6
Josh E Avatar answered Oct 11 '22 23:10

Josh E