6

Hopefully someone can explain this to me. Sorry if it's a repeat, the keywords to explain what I'm seeing are beyond me for now..

here is some code that compiles

class Program
{
    static void Main(string[] args)
    {
        new Transformer<double, double>(Math.Sqrt);
    }
}

class Transformer<Tin, Tout>
{
    Func<Tin, Task<Tout>> actor;
    public Transformer(Func<Tin, Tout> actor)
    {
        this.actor = input => Task.Run<Tout>(() => actor(input));
    }
}

and here is some code that does not

class Program
{
    static void Main(string[] args)
    {
        new Transformer<double, double>(Math.Sqrt);
    }
}

public class Transformer<Tin, Tout>
{
    Func<Tin, Task<Tout>> actor;
    public Transformer(Func<Tin, Tout> actor)
    {
        this.actor = input => Task.Run<Tout>(() => actor(input));
    }

    public Transformer(Func<Tin, Task<Tout>> actor)
    {
        this.actor = actor;
    }
}

By adding the constructor overload, this apparently creates ambiguity but I'm not sure why. Math.Sqrt is not overloaded and clearly has a return type of double, not Task<double>.

Here is the error:

The call is ambiguous between the following methods or properties: 'ConsoleApplication1.Transformer<double,double>.Transformer(System.Func<double,double>)' and 'ConsoleApplication1.Transformer<double,double>.Transformer(System.Func<double,System.Threading.Tasks.Task<double>>)'

Can someone explain why the choice is not obvious to the compiler?


Easy workaround for those who care:

class Program
{
    static void Main(string[] args)
    {
        new Transformer<double, double>(d => Math.Sqrt(d));
    }
}
eisenpony
  • 565
  • 4
  • 10
  • 2
    Btw there is another workaround: `new Transformer((Func) Math.Sqrt);` – Szer Aug 13 '15 at 17:45
  • 1
    Could be a bug. I've been to the limit of type inference a few times myself, and have seen it fail similar to this. I'll tell you what's truly terrible--you're not chaining your constructor calls together. Tut tut. `public Transformer(Func actor) : this(input => Task.Run(() => actor(input)))`. Doubt that would fix, but it'll prevent unfortunate bugs! –  Aug 13 '15 at 17:47
  • @Will fair comment though this is (obviously, I hope) a simple repro. I'm actually seeing this behavior on the Microsoft Dataflow(https://msdn.microsoft.com/en-us/library/hh228603(v=vs.110).aspx) classes: TransformBlock<,> etc... – eisenpony Aug 13 '15 at 23:07
  • See [this answer to the same issue](https://stackoverflow.com/a/33608782/458354) for simple explanation that the compiler needs the delegate to be explicitly typed (as @Szer suggests). – mdisibio Jun 02 '17 at 20:13

1 Answers1

6

You have a slight misinterpretation of how Func<Tin, Tout> works. Take a look at the docs:

public delegate TResult Func<in T, out TResult>(
    T arg
)

The first argument is a parameter and the last argument is the return type.

When you look at this simplified version of your code:

internal class Program
{
    public static void Main(string[] args)
    {
        new MyClass<double, double>(Method);
    }

    private static double Method(double d)
    {
        throw new NotImplementedException();
    }
}


internal class MyClass<T, U>
{
    public MyClass(Func<U, T> arg)
    {
    }

    public MyClass(Func<U, Task<T>> arg)
    {
    }
}

You will notice that both arguments first specify the double, which is an argument, and then differ in return type: the T vs Task<T>.

However as we both know: overloading is done based on method name, parameter arity and parameter types. Return types are entirely ignored. In our case that means we have two Func<Tin, Tout> with double as argument and T vs Task<T> as return type.

Switching the arguments around compiles just fine:

internal class MyClass<T, U>
{
    public MyClass(Func<T, U> arg)
    {
    }

    public MyClass(Func<Task<T>, U> arg)
    {
    }
}

If you'll look in Visual Studio you'll notice that this method is now greyed out, which makes sense because the argument to Method is of type double and as such will always match T and not Task<T>.

So in order to test that it would now hit the correct overload if you pass in a different, asynchronous method, you can add in a second method:

private static double MethodAsync(Task<double> d)
{
   throw new NotImplementedException();
}

and call it using

new MyClass<double, double>(MethodAsync);

You will now notice that the asynchronous Func<Task<T>, U>> is hit (which you can verify by doing a simply print to console from the constructors).


In short: you're trying to perform overload resolution on a return type, which obviously isn't possible.

Jeroen Vannevel
  • 43,651
  • 22
  • 107
  • 170
  • Hmm.. I'm still not getting something. The ambiguity is on the constructors, which have different parameters. There is no ambiguity for Math.Sqrt -- there is only one option. Based on your answer, shouldn't that result in a correct resolution? – eisenpony Aug 13 '15 at 22:45
  • @eisenpony: the ambiguity isn't in Math.Sqrt -- it's in the constructors their Func parameter. You say they take different parameters but they don't: they accept a different return type, but the same arguments. This is a case where it seems like the constructors take different params but you have to look deeper. First it matches for a Func and a double and it finds that. Then it looks inside the Func to see which one it has to use and it finds 2 Funcs that have the same arguments but a different return type so it's stuck there. – Jeroen Vannevel Aug 14 '15 at 16:10
  • Okay I get you. I wasn't suggesting the Func takes different parameters, I thought the constructors take different parameters. However, what you say about the Func's being, from the compiler's POV, indistinguishable from each other makes sense. This is the normal behavior for the compiler because when you call methods normally, it's not possible to anticipate the expected return type. However, in this case it feels like the compiler _could_ have used the return type as part of it's resolution because there is only one possible return type from the Math.Sqrt.method group: double. – eisenpony Aug 14 '15 at 16:42