5

Consider the following program:

using System;
using System.Threading.Tasks;

public class Program
{
    public static void Main()
    {
        var stringTask = Task.FromResult("sample");
        stringTask.TeeAsync(st => Task.CompletedTask).Wait();
    }
}

public static class FunctionalExtensions
{
    public static async Task<T> TeeAsync<T>(this T source, Func<T, Task> asyncAction)
    {       
        await Task.Delay(0); // todo: do something with source

        return source;
    }

    public static async Task<T> TeeAsync<T>(this Task<T> asyncSource, Func<T, Task> asyncAction)
    {
        var source = await asyncSource;

        await Task.Delay(0); // todo: do something with source

        return source;
    }
}

The compiler errors on line 9 where TeeAsync is invoked on stringTask because

The call is ambiguous between the following methods or properties: 'FunctionalExtensions.TeeAsync<T>(T, Func<T, Task>)' and 'FunctionalExtensions.TeeAsync<T>(Task<T>, Func<T, Task>)'

Removing the second parameter from each overload suddenly allows the compiler to distinguish between Task<T> and T for the first parameter. But why does the second parameter -- identical between the two overloads -- cause the compiler to get confused?

6
  • @maccettura changing the compiler error message from a code sample to a blockquote has caused some of the details of the error message to be hidden e.g. the generic type arguments of the methods and classes referenced in the error message. Commented Mar 30, 2018 at 19:02
  • I’m not seeing anything change except the format. Look at the edit history, I removed 4 white space characters and added >. Anything that is not in your question is because it never was. Commented Mar 30, 2018 at 19:04
  • I know what you edited. I had looked before. Now please look at the actual text that you see in the edit view and compare to what you see on this rendered page. The types are missing. Trust me. Commented Mar 30, 2018 at 19:10
  • Ah I missed that. I thought you were talking about extra information not being there (like more to the error message itself). Look better now? Your code block was hard to read before because its just one long scroll-able field, now we can see everything in the viewport. Commented Mar 30, 2018 at 19:13
  • Ya that looks right now. Annoying that blockquote requires special encoding. Commented Mar 30, 2018 at 19:17

1 Answer 1

4

Second parameters are not identical. They are both Func<T, Task>, but T is different in each case.

First overload has this T source. That means when you do

Task<string> stringTask = Task.FromResult("sample");
stringTask.TeeAsync(...)

for first overload, T is Task<string>.

Second has this Task<T> asyncSource. So in above case, for second overload T is string.

Because you don't specify type of st here:

stringTask.TeeAsync(st => Task.CompletedTask).Wait();

st can be either Task<string> (first overload) or string (second). Compiler cannot know which one you meant. If you do:

stringTask.TeeAsync((string st) => Task.CompletedTask).Wait();

It will correctly choose second one. And if you do

stringTask.TeeAsync((Task<string> st) => Task.CompletedTask).Wait();

it will choose first.

Interesting that if you actually use st in a way which will allow compiler to deduce whether it's string or Task<string> - it will do that. For example this will compile and choose second overload:

// we don't specify st type, but using Length property
// which only exists on string
stringTask.TeeAsync(st => Task.FromResult(st.Length)).Wait();

And this will compile and choose first:

// we don't specify st type, but using Result property
// which only exists on Task<string>
stringTask.TeeAsync(st => Task.FromResult(st.Result)).Wait();

But if you use something that exists on both, it will again (correctly) fail to choose an overload:

// ToString() exists on both string and Task<string>
// so doesn't help compiler to choose
stringTask.TeeAsync(st => Task.FromResult(st.ToString())).Wait();
Sign up to request clarification or add additional context in comments.

5 Comments

Do you have any advice on refactoring the methods themselves in order to avoid the calling overhead of specifying what the type of T is?
@bugged87 hard to tell, because it depends on what those functions actually do (I mean what's the goal of them, not implementation details), and we now have only example stubs.
They are just plumbing to add support for a functional pipeline using C# method chaining. See this post by Dave Fancher.
@bugged87 for this specific methods in your question nothing comes to mind so far (except as you can see from my last edit - what you actually do in asyncAction might help compiler to figure it out without caller specifying type of st). As for MapAsync from your link - it should not be an issue there, because TResult helps with that.
Correct, I only have the issue with the TeeAsync overloads. Great answer though. I didn't think to test the inference engine by adding additional context like you did. It's likely that real-world code will have this context and ambiguity won't be an issue after all.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.