locked
Why not infer Task<T>?

    Question

  • In a method signature of:

    async Task<int> Blah() { return 5; },

    there is a mental disconnect in returning int when a Task<int> is required.  Since the async keyword is used to tell the compiler to 'lift' the int in to a task, could not it also be used to imply the Task type in the method signature, therefore, the above would become:

    async int Blah { return 5; }

    and

    var t1 = await Blah()   would make t1 an int, while

    var t2 = Blah() would make t2 a Task<int>.

    Regards,

    Scott

     

     

    Wednesday, May 4, 2011 2:21 AM

Answers

  • scotteg,

    We definitely considered this design in the initial (and even later) stages of developing the Async feature. Ultimately there were a lot of reasons for settling on the current design, some of which have already been mentioned.

    Originally, only the Visual Basic design for Async required the use of the Async keyword (Visual Basic also has an Iterator keyword).

    For C# we also chose to follow a design very similar to iterators where the return type and the presense of certain constructs in the method body would determine whether it was "async" or not. For iterators we don't hide the fact that the method returns an IEnumerable<T> (though the method never explicitly returns an object of type IEnumerable<T>). Similar to iterators we felt that whether a method was implemented with a state-machine transformation was ultimately an implementation detail; consumers wouldn't be able to tell (and shouldn't need to know) what voodoo compiler magic is used under the covers only that this method returns a Task and Tasks can be awaited.

    We later decided to use the async keyword because it solved other problems like back-compat for the await keyword and some ambiguities involing lambda-expressions. At that point (especially in VB) we reconsidered hiding the Task type as well but felt that understanding of Tasks was very critical to the use of Async. When we factored in methods on the Task (or TaskEx) type like WaitAny, WaitAll, Delay, Run, etc - we felt even more strongly that diminishing the Task type was leading users in the wrong direction. Also there wasn't a very clear (or worthwhile) IDE experience around hiding task at the definition site but showing it in IntelliSense when you consumed it elsewhere in your program or the object-browser, etc. It wouldn't have made sense to, in IntelliSense show every method which return Task or was awaitable (Task isn't the only awaitable type, btw) as async but that's what it would have taken to be consistent with that design - especially as we also cannot tell if a method in a compiled assembly was implemented using async or not. Also, if you assign the result of an "async int Blah()" to a variable: var blahing = Blah(); what would the type of blahing be? Task<int>, of course. So if the result is Task<int> why pretend it's not "If Stop signs are red why should we say they have no color?".

    While we definitely weren't unphased by a certain initial disconnect as you describe we grew to appreciate that the alternatives had worse disconnects without enough value-add to justify the effort. That left us with the current design which we felt delivered the most value while being consistent with the rest of the language in both declaring and consuming async methods.

    Does that answer your question?

    -ADG

     

    Thursday, July 21, 2011 4:26 AM

All replies

  • I think it is actually clearer the way it is already implemented in the existing CTP. 
    Wednesday, May 4, 2011 3:00 PM
  • While inferring the Task<T> due to async would be fine when looking at your code, it would cause the code to have a different signature during development than it would have after compilation.

     

    A consumer of this routine (ie: if it was compiled into a library) has no direct way to determine whether the method is defined using async - this is purely an implementation detail.  The consumer of your library will always see this as a method defined as:

        Task<int> Blah();

     

    This is true whether you use async/await to implement this method, or whether you directly return a Task<int> generated by other means.

     

    Since the method would have this signature when used by another assembly, I personally prefer it have this signature internally for consistency.

     


    Reed Copsey, Jr. - http://reedcopsey.com
    If a post answers your question, please click "Mark As Answer" on that post and "Mark as Helpful".
    Monday, May 9, 2011 4:29 PM
    Moderator
  • >Since the method would have this signature when used by another assembly, I personally prefer it have this signature internally for consistency.

     

    Surely the async keyword is already responsible for a substantial transformation between what you write and what ends up being generated ? 

    Learning that signature

     

    async public type func ()

     

    is synonymous with the signature

     

    public Task<type> func() 

     

    would seem to be the least of the mental contortions one needs to go through in understanding async ;) (You could argue that this would 'bake' the Task type too deeply into the language but that's already been done through 'await' as far as I can see.)  

     

    I think the most practical objection is that it makes the following rather difficult to implement....

     

    Task<int> NestedAsync(void)
    {
     return TaskEx.Run(() => {Thread.Sleep(10000);return 1;}) ;
    }

    //a synchronous method int aLongCalculation() { //Lots of work here }

    //Hmm - if async 'taskifies' the return type, we have a problem creating and returning
    //a new task in this method
    async int SwitchedAsync(int mechanism) {
    switch (mechanism) {
    case 0: return TaskEx.Run(() => 2) ; // <== this would need to be an int, not a task case 1: return await NestedAsync() ;
    default; return 3;
     }
    }

     





    • Edited by nmacmull Monday, May 9, 2011 8:22 PM better example
    Monday, May 9, 2011 8:18 PM
  • scotteg,

    We definitely considered this design in the initial (and even later) stages of developing the Async feature. Ultimately there were a lot of reasons for settling on the current design, some of which have already been mentioned.

    Originally, only the Visual Basic design for Async required the use of the Async keyword (Visual Basic also has an Iterator keyword).

    For C# we also chose to follow a design very similar to iterators where the return type and the presense of certain constructs in the method body would determine whether it was "async" or not. For iterators we don't hide the fact that the method returns an IEnumerable<T> (though the method never explicitly returns an object of type IEnumerable<T>). Similar to iterators we felt that whether a method was implemented with a state-machine transformation was ultimately an implementation detail; consumers wouldn't be able to tell (and shouldn't need to know) what voodoo compiler magic is used under the covers only that this method returns a Task and Tasks can be awaited.

    We later decided to use the async keyword because it solved other problems like back-compat for the await keyword and some ambiguities involing lambda-expressions. At that point (especially in VB) we reconsidered hiding the Task type as well but felt that understanding of Tasks was very critical to the use of Async. When we factored in methods on the Task (or TaskEx) type like WaitAny, WaitAll, Delay, Run, etc - we felt even more strongly that diminishing the Task type was leading users in the wrong direction. Also there wasn't a very clear (or worthwhile) IDE experience around hiding task at the definition site but showing it in IntelliSense when you consumed it elsewhere in your program or the object-browser, etc. It wouldn't have made sense to, in IntelliSense show every method which return Task or was awaitable (Task isn't the only awaitable type, btw) as async but that's what it would have taken to be consistent with that design - especially as we also cannot tell if a method in a compiled assembly was implemented using async or not. Also, if you assign the result of an "async int Blah()" to a variable: var blahing = Blah(); what would the type of blahing be? Task<int>, of course. So if the result is Task<int> why pretend it's not "If Stop signs are red why should we say they have no color?".

    While we definitely weren't unphased by a certain initial disconnect as you describe we grew to appreciate that the alternatives had worse disconnects without enough value-add to justify the effort. That left us with the current design which we felt delivered the most value while being consistent with the rest of the language in both declaring and consuming async methods.

    Does that answer your question?

    -ADG

     

    Thursday, July 21, 2011 4:26 AM