7

The Problem

Consider these two extension methods which are just a simple map from any type T1 to T2, plus an overload to fluently map over Task<T>:

public static class Ext {
    public static T2 Map<T1, T2>(this T1 x, Func<T1, T2> f)
       => f(x);
    public static async Task<T2> Map<T1, T2>(this Task<T1> x, Func<T1, T2> f)
        => (await x).Map(f);
}

Now, when I use the second overload with a mapping to a reference type...

var a = Task
    .FromResult("foo")
    .Map(x => $"hello {x}"); // ERROR

var b = Task
    .FromResult(1)
    .Map(x => x.ToString()); // ERROR

...I get the following error:

CS0121: The call is ambiguous between the following methods or properties: 'Ext.Map(T1, Func)' and 'Ext.Map(Task, Func)'

Mapping to a value type works fine:

var c = Task
    .FromResult(1)
    .Map(x => x + 1); // works

var d = Task
    .FromResult("foo")
    .Map(x => x.Length); // works

But only as long the mapping actually uses the input to produce an output:

var e = Task
    .FromResult(1)
    .Map(_ => 0); // ERROR

The Question

Can anyone please explain to me what is going on here? I've already given up on finding a feasible fix for this error, but at least I'd like to understand the root cause of this mess.

Additional Notes

So far I found three workarounds which are unfortunately not acceptable in my use case. The first is to specify the type arguments of Task<T1>.Map<T1,T2>() explicitly:

var f = Task
    .FromResult("foo")
    .Map<string, string>(x => $"hello {x}"); // works

var g = Task
    .FromResult(1)
    .Map<int, int>(_ => 0); // works

Another workaround is to not use lambdas:

string foo(string x) => $"hello {x}";
var h = Task
    .FromResult("foo")
    .Map(foo); // works

And the third option is to restrict the mappings to endofunctions (i.e. Func<T, T>):

public static class Ext2 {
    public static T Map2<T>(this T x, Func<T, T> f)
        => f(x);
    public static async Task<T> Map2<T>(this Task<T> x, Func<T, T> f)
        => (await x).Map2(f);
}

I created a .NET Fiddle where you can try out all the above examples yourself.

Mureinik
  • 297,002
  • 52
  • 306
  • 350
Good Night Nerd Pride
  • 8,245
  • 4
  • 49
  • 65
  • 1
    Please, have a look at the following threads, [Overloaded method-group argument confuses overload resolution](https://stackoverflow.com/questions/5203792/overloaded-method-group-argument-confuses-overload-resolution) and [Why is Func ambiguous with Func>](https://stackoverflow.com/questions/4573011/why-is-funct-ambiguous-with-funcienumerablet) I guess that main idea is similar with your question – Pavel Anikhouski Mar 19 '20 at 12:29
  • 1
    Those were interesting reads, but I don't see the a strong connection to my problem apart from the error message. Can someone call Eric Lippert, please? :D – Good Night Nerd Pride Mar 19 '20 at 17:57
  • 1
    To the anonymous close voter: please explain to me how I can make this question more focused. What exactly in "Here is a compiler error and some minimal example code. Please help me make it got away or at least help me understand what's wrong here." is not focused enough? – Good Night Nerd Pride Mar 20 '20 at 10:33
  • 1
    Forcing clients of the API to almost always specify type arguments that usually can be inferred seems more like an ugly work-around to me. Anyway, I finally found a more minimal example and will rewrite the entire question. – Good Night Nerd Pride Mar 20 '20 at 11:31
  • 1
    @GoodNightNerdPride Please, refer to this [question](https://stackoverflow.com/questions/4014036/ambiguous-call-between-two-c-sharp-extension-generic-methods-one-where-tclass-a). I think that it is related to your problem. – Iliar Turdushev Mar 20 '20 at 13:05
  • @IliarTurdushev, that also was an interesting read, but I don't think it pertains to my question directly. Although both are similar in nature. However, I'm unable to reproduce the problem in your linked question with C# 8, because [overload resolution changed since then](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-7.3/improved-overload-candidates). – Good Night Nerd Pride Mar 21 '20 at 08:34
  • By convention the second extension method (the one with `async` keyword) should be named `MapAsync`, which would get rid of this error. See [here](https://stackoverflow.com/questions/15951774/does-the-use-of-the-async-suffix-in-a-method-name-depend-on-whether-the-async) for some discussion about this. – Grx70 Mar 22 '20 at 10:02
  • @Grx70 No, I can't use the the `Async` suffix here, because I need it for asynchronous mapping functions: i.e. for `T1.MapAsync(Func>)` and for `Task.MapAsync(Func>)`. – Good Night Nerd Pride Mar 22 '20 at 10:12

3 Answers3

3

According to C# Specification, Method invocations, the next rules are used to consider a generic method F as a candidate for method invocation:

  • Method has the same number of method type parameters as were supplied in the type argument list,

    and

  • Once the type arguments are substituted for the corresponding method type parameters, all constructed types in the parameter list of F satisfy their constraints (Satisfying constraints), and the parameter list of F is applicable with respect to A (Applicable function member). A - optional argument list.

For expression

Task.FromResult("foo").Map(x => $"hello {x}");

both methods

public static T2 Map<T1, T2>(this T1 x, Func<T1, T2> f);
public static async Task<T2> Map<T1, T2>(this Task<T1> x, Func<T1, T2> f);

satisfy these requirements:

  • they both have two type parameters;
  • their constructed variants

    // T2 Map<T1, T2>(this T1 x, Func<T1, T2> f)
    string       Ext.Map<Task<string>, string>(Task<string>, Func<Task<string>, string>);
    
    // Task<T2> Map<T1, T2>(this Task<T1> x, Func<T1, T2> f)
    Task<string> Ext.Map<string, string>(Task<string>, Func<string, string>);
    

satisfy type constraints (because there is no type constraints for Map methods) and applicable according to optional arguments (because also there is no optional arguments for Map methods). Note: to define the type of the second argument (lambda expression) a type inference is used.

So at this step the algorithm considers both variants as candidates for method invocation. For this case it uses Overload resolution to determine which candidate better fits for invocation. Words from specification:

The best method of the set of candidate methods is identified using the overload resolution rules of Overload resolution. If a single best method cannot be identified, the method invocation is ambiguous, and a binding time error occurs. When performing overload resolution, the parameters of a generic method are considered after substituting the type arguments (supplied or inferred) for the corresponding method type parameters.

Expression

// I intentionally wrote it as static method invocation.
Ext.Map(Task.FromResult("foo"), x => $"hello {x}");

can be rewritten the next way using constructed variants of the method Map:

Ext.Map<Task<string>, string>(Task.FromResult("foo"), (Task<string> x) => $"hello {x}");
Ext.Map<string, string>(Task.FromResult("foo"), (string x) => $"hello {x}");

Overload resolution uses Better function member algorithm to define which of this two methods better fits method invocation.

I have read this algorithm several times and haven't found a place where the algorigthm can define the method Exp.Map<T1, T2>(Task<T1>, Func<T1, T2>) as better method for considered method invocation. In this case (when better method cannot be defined) a compile time error occures.

To sum up:

  • method invocation algorithm considers both methods as candidates;
  • better function member algorithm cannot define better method to invoke.

Another approach of helping compiler to choose better method (as you did in your other workarounds):

// Call to: T2 Map<T1, T2>(this T1 x, Func<T1, T2> f);
var a = Task.FromResult("foo").Map( (string x) => $"hello {x}" );

// Call to: async Task<T2> Map<T1, T2>(this Task<T1> x, Func<T1, T2> f);
var b = Task.FromResult(1).Map( (Task<int> x) => x.ToString() );

Now the first type argument T1 is explicitly defined and an ambiguity does not occur.

Iliar Turdushev
  • 4,935
  • 1
  • 10
  • 23
  • This is a good answer, but there is "better" workaround :) See my answer. "Better" means less typing in this case. – weichch Mar 22 '20 at 01:52
2

In overload resolution, the compiler will infer type arguments if not specified.

In all the error cases, the input type T1 in Fun<T1, T2> is ambiguous. For example:

Both Task<int>, and int have ToString method, so there is no way to infer whether it is task or int.

However if + is used in expression, it is clear that the input type is integer because task does not support + operator. .Length is the same story.

This can also explain other errors.

UPDATE

The reason for passing Task<T1> won't make the compiler pick up the method with Task<T1> in argument list is the compiler needs to take effort to infer T1 out of Task<T1> because T1 is not directly in the method's argument list.

Possible fix: Make Func<> to use what is existing in the method's argument list, so the compiler takes less effort when inferring T1.

static class Extensions
{
    public static T2 Map<T1, T2>(this T1 obj, Func<T1, T2> func)
    {
        return func(obj);
    }

    public static T2 Map<T1, T2>(this Task<T1> obj, Func<Task<T1>, T2> func)
    {
        return func(obj);
    }
}

Usage:

// This calls Func<T1, T2>
1.Map(x => x + 1);

// This calls Func<Task<T1>, T2>
Task.FromResult(1).Map(async _=> (await _).ToString())

// This calls Func<Task<T1>, T2>
Task.FromResult(1).Map(_=> 1)

// This calls Func<Task<T1>, T2>.
// Cannot compile because Task<int> does not have operator '+'. Good indication.
Task.FromResult(1).Map(x => x + 1)
weichch
  • 9,306
  • 1
  • 13
  • 25
  • Interesting, I didn't think about that. But I still don't quite get it. The compiler knows I'm calling `Map()` on a `Task`. Why would it even try the other overload? And why exactly is the ambiguity resolved when I assist the type inference? – Good Night Nerd Pride Mar 21 '20 at 13:27
  • Because `Task` can match against `Map,X>`. eg `T1` == `Task`. Easiest workaround, rename your async method to MapAsync... – Jeremy Lakeman Mar 21 '20 at 13:34
  • As stated in the comments to the other answer, renaming to `MapAsync` is not an option, because I need that suffix for async _mapping_ functions (e.g. `T1.MapAsync(Func)`). – Good Night Nerd Pride Mar 21 '20 at 14:13
  • I'm aware that a `T1` can be a `Task`, but AFAIK overload resolution will favor the most specific parameter type, which in my case is `Task`. – Good Night Nerd Pride Mar 21 '20 at 14:15
  • @GoodNightNerdPride most specific parameter is only true if there is only one case, but in your case, there are two. – Akash Kava Mar 21 '20 at 16:06
  • @GoodNightNerdPride You can think of overload resolution like a smart lazy guy. Less efforts take wins. It is same effort the compiler needs to take to infer `T1` and put it into `Func` as infer `T1` from `Task` and put it into `Func`. Because for the second case, `T1` is not in the argument list of the method, so compiler needs to work out and get it from `Task`. To fix that, you can make `Fun<>` to use existing parameters in the method's argument list, and it will reduce the effort taken. See updated answer for possible solution. – weichch Mar 22 '20 at 01:09
0

add braces

var result = (await Task
            .FromResult<string?>("test"))
            .Map(x => $"result: {x}");

your FilterExt async method is just adding braces to (await x) and then calls the non async method, so what for do you need async method??

UPDATE: As i noticed in many .net libraries, developers just adding Async suffix to async methods. You can name method MapAsync, FilterAsync