Block constructor overloads accepting Tasks defeat type inference

Answered Block constructor overloads accepting Tasks defeat type inference

  • Thursday, April 14, 2011 9:31 AM
     
      Has Code

    In my experiments with the TPL Dataflow library, I've run into a small but irritating problem a couple of times now. Some of the built-in blocks have constructors that accept delegates. Some of these optionally allow the delegates to return a Task instead of a plain value. While I can see that this could be useful, it has the unfortunate side effect of causing C# to be unable to infer the delegate type directly even when the inference should be unambiguous.

    This is really down to a shortcoming in C# - it has limitations around infering the return type of a delegate from a method group. These limitations ensure consistency in more complex scenarios, but cause a failure in certain simple scenarios. And the TPL Dataflow library runs straight into some of these. For example, I have a method that looks like this:

    private float[] ProcessBlocks(float[][] arg)
    {
     var output = new float[arg.Last().Length];
     // do some work...
     return output;
    }
    

    And I want to use this with a TransformBlock. But trying this:

    _transformBlock = new TransformBlock<float[][], float[]>(ProcessBlocks);
    

    I get a compiler error:

     error CS0121: The call is ambiguous between the following methods or properties: 'System.Threading.Tasks.Dataflow.TransformBlock<float[][],float[]>.TransformBlock(System.Func<float[][],float[]>)' and 'System.Threading.Tasks.Dataflow.TransformBlock<float[][],float[]>.TransformBlock(System.Func<float[][],System.Threading.Tasks.Task<float[]>>)'

     Note that I'm not actually asking it to infer any of the type arguments here. They are all explicitly called out there - my TransformBlock's input is an array of arrays of floats, and the output is an array of floats. The problem is that the compiler thinks it has found two candidate overloads:

    public TransformBlock<TInput, TOutput>(
     Func<TInput, TOutput> transform
    )
    

    or 

    public TransformBlock<TInput, TOutput>(
     Func<TInput, Task<TOutput>> transform
    )
    

    By inspection, clearly only one of these can work - the second one is not a viable candidate because there's no way my ProcessBlocks method can be referred to by a delegate that requires the return type to be Task<Something> - my method's return type is float[], meaning that only that first option is a candidate. However, because of the rather complicated way in which delegate type inference and method overload resolution work in C#, the compiler actually gives up before it has enough information to work out that there is only one valid solution here.

    So I have to write this:

    _transformBlock = new TransformBlock<float[][], float[]>((Func<float[][], float[]>) ProcessBlocks);
    

    and I think that's significantly uglier and harder to read than what I'd like to be able to write.

    Fundamentally the problem here is that overloads taking delegates that differ only by return type don't sit well with C#. A way around this might be to move to factory methods?


    • Edited by IanGMVP Friday, April 15, 2011 2:57 PM changed "ambiguous" to "unambiguous" in 1st para
    •  

All Replies

  • Friday, April 15, 2011 2:48 PM
    Owner
     
     Answered

    Hi Ian-

    Thanks for taking the time to highlight this issue.  I agree it's a bit cumbersome.  It's also something we've collectively run into outside of TPL Dataflow in the core of TPL itself, namely with Task.Run, which shows up in the Async CTP as Run and RunEx... the reason for those two separate methods is exactly the issue you've highlighted, that the compiler can't currently disambiguate between the relevant signatures.

    Not for the CTP, but this overload resolution issue has been addressed for vNext, at least for lambdas.  This means that while you'll currently still have trouble with method groups, e.g.

        new TransformBlock<float[][],float[]>(ProcessBlocks);

    the equivalent with lambdas will work fine:

        new TransformBlock<float[][],float[]>(arg => ProcessBlocks(arg));

    We're still investigating whether there's anything we can do for method groups to address this problem.  If we're unable to do something at the compiler level, we'll definitely consider adjusting the API, though it's not clear what the right design would be... potentially factory methods.  Of course, if you find yourself running into this a lot, you can also build your own one-line factory methods to handle this , supplying different names for the methods accepting the different delegate types.

    Also, Lucian would be disappointed in me if I didn't mention that this all works "correctly" with VB ;)

    Thanks, again.

  • Friday, April 15, 2011 2:57 PM
     
     

    Fixing it at the compiler level would certainly be nice, but I thought that might be a bit much to ask of the TPL Dataflow team. :)

    As for lambdas...having gone lambda crazy a few years ago, I've tended to move slightly back in the direction of putting things into named methods for this sort of thing, not least because it tends to make it a lot easier to follow in the debugger. The Task-aware bits of the debugger will sometimes end up showing you the name of the method that the task will run as a sort of identifier for a task. When that's some compiler-generated internal lambda name, it can be harder to see what's happening.

  • Thursday, June 02, 2011 6:16 PM
     
     

    IanG,

    Thanks for the explanation and the turnaround, almost always I prefer group methods to lambdas (and I have the same reasons). Your post is helpful.


  • Thursday, March 08, 2012 9:22 PM
     
      Has Code

    I find that this is still a problem with VS11 Beta.  I have the same problem with

    _outputEntities = new TransformBlock<DataEntityBuffer, DataEntityBuffer>(b => TransformBuffer(b), options);

    : public DataEntityBuffer TransformBuffer(DataEntityBuffer inputBuffer) { DataEntityBuffer outputbuffer;        

         :

    return outputBuffer; }

    I have found no code with this intent that will compile if the options argument is included.  Regardless of what I try, the compiler thinks the delegate should return Task<DataEntityBuffer>. 

    Rob


    • Edited by mount77 Friday, March 09, 2012 6:07 PM Removed info about failure if options are specified. I was using DataflowBlockOptions instead of ExecutionDataflowBlockOptions. However, the type inference problem persists in the beta.
    •