The C# language design have always (historically) been geared towards solving specific problems rather then finding to address the underlying general problems: see for example http://blogs.msdn.com/b/ericlippert/archive/2009/07/09/iterator-blocks-part-one.aspx for "IEnumerable vs. coroutines":
We could have made it much more general. Our iterator blocks can be seen as a weak kind of coroutine. We could have chosen to implement full coroutines and just made iterator blocks a special case of coroutines. And of course, coroutines are in turn less general than first-class continuations; we could have implemented continuations, implemented coroutines in terms of continuations, and iterators in terms of coroutines.
or http://blogs.msdn.com/b/wesdyer/archive/2008/01/11/the-marvels-of-monads.aspx for SelectMany as a surrogate for (some kind of) Monads:
The C# type system is not powerful enough to create a generalized abstraction for monads which was the primary motivator for creating extension methods and the "query pattern"
I do not want to ask why has been so (many good answers have been already given, especially in Eric's blog, which may apply to all these design decisions: from performance to increased complexity, both for the compiler and the programmer).
What I am trying to understand is to which "general construct" the async/await keywords relate to (my best guess is the continuation monad - after all, F# async is implemented using workflows, which to my understanding is a continuation monad), and how they relate to it (how they differ?, what is missing?, why there is a gap, if any?)
I'm looking for an answer similar to the Eric Lippert article I linked, but related to async/await instead of IEnumerable/yield.
Edit: besides the great answers, some useful links to related questions and blog posts where suggested, I'm editing my question to list them:
- A starting point for
bind
using await
- Implementation details of the state machine behind await
- Other details on how await gets compiled/rewritten
- Alternative, hypothetical implementation using continuations (call/cc)
The asynchronous programming model in C# is very similar to asynchronous workflows in F#, which are an instance of the general monad pattern. In fact, the C# iterator syntax is also an instance of this pattern, although it needs some additional structure, so it is not just simple monad.
Explaining this is well beyond the scope of a single SO answer, but let me explain the key ideas.
Monadic operations.
The C# async essentially consists of two primitive operations. You can await
an asynchronous computation and you can return
the result from an asynchronous computation (in the first case, this is done using a new keyword, while in the second case, we're re-using a keyword that is already in the language).
If you were following the general pattern (monad) then you would translate the asynchronous code into calls to the following two operations:
Task<R> Bind<T, R>(Task<T> computation, Func<T, Task<R>> continuation);
Task<T> Return<T>(T value);
They can both be quite easily implemented using the standard task API - the first one is essentially a combination of ContinueWith
and Unwrap
and the second one simply creates a task that returns the value immediately. I'm going to use the above two operations, because they better capture the idea.
Translation. The key thing is to translate asynchronous code to normal code that uses the above operations.
Let's look at a case when we avait an expression e
and then assign the result to a variable x
and evaluate expression (or statement block) body
(in C#, you can await inside expression, but you could always translate that to code that first assigns the result to a variable):
[| var x = await e; body |]
= Bind(e, x => [| body |])
I'm using a notation that is quite common in programming languages. The meaning of [| e |] = (...)
is that we translate the expression e
(in "semantic brackets") to some other expression (...)
.
In the above case, when you have an expression with await e
, it is translated to the Bind
operation and the body (the rest of the code following await) is pushed into a lambda function that is passed as a second parameter to Bind
.
This is where the interesting thing happens! Instead of evaluating the rest of the code immediately (or blocking a thread while waiting), the Bind
operation can run the asynchronous operation (represented by e
which is of type Task<T>
) and, when the operation completes, it can finally invoke the lambda function (continuation) to run the rest of the body.
The idea of the translation is that it turns ordinary code that returns some type R
to a task that returns the value asynchronously - that is Task<R>
. In the above equation, the return type of Bind
is, indeed, a task. This is also why we need to translate return
:
[| return e |]
= Return(e)
This is quite simple - when you have a resulting value and you want to return it, you simply wrap it in a task that immediately completes. This might sound useless, but remember that we need to return a Task
because the Bind
operation (and our entire translation) requires that.
Larger example. If you look at a larger example that contains multiple await
s:
var x = await AsyncOperation();
return await x.AnotherAsyncOperation();
The code would be translated to something like this:
Bind(AsyncOperation(), x =>
Bind(x.AnotherAsyncOperation(), temp =>
Return(temp));
The key trick is that every Bind
turns the rest of the code into a continuation (meaning that it can be evaluated when an asynchronous operation is completed).
Continuation monad. In C#, the async mechanism is not actually implemented using the above translation. The reason is that if you focus just on async, you can do a more efficient compilation (which is what C# does) and produce a state machine directly. However, the above is pretty much how asynchronous workflows work in F#. This is also the source of additional flexibility in F# - you can define your own Bind
and Return
to mean other things - such as operations for working with sequences, tracking logging, creating resumable computations or even combining asynchronous computations with sequences (async sequence can yield multiple results, but can also await).
The F# implementation is based on the continuation monad which means that Task<T>
(actually, Async<T>
) in F# is defined roughly like this:
Async<T> = Action<Action<T>>
That is, an asynchronous computation is some action. When you give it Action<T>
(a continuation) as an argument, it will start doing some work and then, when it eventually finishes, it invokes this action that you specified. If you search for continuation monads, then I'm sure you can find better explanation of this in both C# and F#, so I'll stop here...
Tomas's answer is very good. To add a few more things:
The C# language design have always (historically) been geared towards solving specific problems rather then finding to address the underlying general problems
Though there is some truth to that, I don't think it is an entirely fair or accurate characterization, so I'm going to start my answer by denying the premise of your question.
It is certainly true that there is a spectrum with "very specific" on one end and "very general" on the other, and that solutions to specific problems fall on that spectrum. C# is designed as a whole to be a highly general solution to a great many specific problems; that's what a general-purpose programming language is. You can use C# to write everything from web services to XBOX 360 games.
Since C# is designed to be a general-purpose programming language, when the design team identifies a specific user problem they always consider the more general case. LINQ is an excellent case in point. In the very early days of the design of LINQ, it was little more than a way to put SQL statements in a C# program, because that's the problem space that was identified. But quite soon in the design process the team realized that the concepts of sorting, filtering, grouping and joining data applied not just to tabular data in a relational database, but also hierarchical data in XML, and to ad-hoc objects in memory. And so they decided to go for the far more general solution that we have today.
The trick of design is figuring out where on the spectrum it makes sense to stop. The design team could have said, well, the query comprehension problem is actually just a specific case of the more general problem of binding monads. And the binding monads problem is actually just a specific case of the more general problem of defining operations on higher kinds of types. And surely there is some abstraction over type systems... and enough is enough. By the time we get to solving the bind-an-arbitrary-monad problem, the solution is now so general that the line-of-business SQL programmers who were the motivation for the feature in the first place are completely lost, and we haven't actually solved their problem.
The really major features added since C# 1.0 -- generic types, anonymous functions, iterator blocks, LINQ, dynamic, async -- all have the property that they are highly general features useful in many different domains. They can all be treated as specific examples of a more general problem, but that is true of any solution to any problem; you can always make it more general. The idea of the design of each of these features is to find the point where they can't be made more general without confusing their users.
Now that I've denied the premise of your question, let's look at the actual question:
What I am trying to understand is to which "general construct" the async/await keywords relate to
It depends on how you look at it.
The async-await feature is built around the Task<T>
type, which is as you note, a monad. And of course if you talked about this with Erik Meijer he would immediately point out that Task<T>
is actually a comonad; you can get the T
value back out the other end.
Another way to look at the feature is to take the paragraph you quoted about iterator blocks and substitute "async" for "iterator". Asynchronous methods are, like iterator methods, a kind of coroutine. You can think of Task<T>
as just an implementation detail of the coroutine mechanism if you like.
A third way to look at the feature is to say that it is a kind of call-with-current-continuation (commonly abbreviated call/cc). It is not a complete implementation of call/cc because it does not take the state of the call stack at the time that the continuation is signed up into account. See this question for details:
How could the new async feature in c# 5.0 be implemented with call/cc?
I will wait and see if someone (Eric? Jon? maybe you?) can fill in more details on how actually C# generates code to implement await,
The rewrite is essentially just a variation on how iterator blocks are rewritten. Mads goes through all the details in his MSDN Magazine article:
http://msdn.microsoft.com/en-us/magazine/hh456403.aspx