A few years back, Wes Dyer wrote a great post on monads, and more recently, Eric Lippert wrote a terrific blog series exploring monads and C#. In that series, Eric alluded to Task
As both Wes and Eric highlight, a monad is a triple consisting of a type, a Unit function (often called Return), and a Bind function. If the type in question is Task
The Unit operator takes a T and “amplifies” it into an instance of the type:
public staticM
Unit (this T value);
That’s exactly what Task.FromResult does, producing a Task
public staticTask
Unit (this T value)
{
returnTask.FromResult(value);
}
What about Bind? The Bind operator takes the instance of our type, extracts the value from it, runs a function over that value to get a new amplified value, and returns that amplified value:
public staticM
Bind(
thisM m, FuncM> k);
If you squint at this, and if you’ve read my previous blog post Implementing Then with Await, the structure of this declaration should look eerily like the last overload of Then discussed:
public staticTask
Then (
thisTasktask, Func Task > continuation);
In fact, other than the symbols chosen, they’re identical, and we can implement Bind just as we implemented Then:
public static asyncTask
Bind(
thisTask m, FuncTask> k)
{
return await k(await m);
}
This is possible so concisely because await and async are so close in nature to the monadic operators. When you write an async function that returns Task
In Eric’s last post on monads, he talks about some of the C# LINQ operators, and how they can easily be implemented on top of types that correctly implement a Unit and a Bind method:
staticM
SelectMany( > function,
thisM monad,
FuncM
Func projection)
{
return monad.Bind(
outer => function(outer).Bind(
inner => projection(outer, inner).Unit()));
}
Sure enough, with our Bind and Unit implementations around Task
staticTask
SelectMany( {
thisTaskmonad,
FuncTask> function,
Func projection)
return monad.Bind(
outer => function(outer).Bind(
inner => projection(outer, inner).Unit()));
}
What does it mean here to “just work”? It means we can start writing LINQ queries using the C# query comprehension syntax with operators that rely on SelectMany, e.g.
int c = await (from first inTask.Run(() => 1)
from second inTask.Run(() => 2)
select first + second);
Console.WriteLine(c == 3); // will output True
Of course, we can implement SelectMany without Bind and Unit, just using async/await directly:
staticTask
SelectMany( > function,
thisTask task,
FuncTask
Func projection)
{
A a = await task;
B b = await function(a);
return projection(a, b);
}
In fact, we can implement many of the LINQ query operators simply and efficiently using async/await. The C# specification section 7.16.3 lists which operators we need to implement to support all of the C# query comprehension syntax, i.e. all of the LINQ contextual keywords in C#, such as select and where. Some of these operators, like OrderBy, make little sense when dealing with singular values as we have with Task
public static asyncTask
SelectMany > selector, Func(
thisTasksource, Func Task resultSelector)
{
T t = await source;
U u = await selector(t);
return resultSelector(t, u);
}
public static asyncTask Select( public static asyncTask
thisTasksource, Func selector)
{
T t = await source;
return selector(t);
}Where > predicate)(
thisTasksource, Func bool
{
T t = await source;
if (!predicate(t)) throw newOperationCanceledException();
return t;
}
public static asyncTaskJoin (
thisTasksource, Task inner,
FuncouterKeySelector, Func innerKeySelector,
FuncresultSelector)
{
awaitTask.WhenAll(source, inner);
if (!EqualityComparer.Default.Equals(
outerKeySelector(source.Result), innerKeySelector(inner.Result)))
throw newOperationCanceledException();
return resultSelector(source.Result, inner.Result);
}
public static asyncTaskGroupJoin (
thisTasksource, Task inner,
FuncouterKeySelector, Func innerKeySelector,
FuncTask, V> resultSelector)
{
awaitTask.WhenAll(source, inner);
if (!EqualityComparer.Default.Equals(
outerKeySelector(source.Result), innerKeySelector(inner.Result)))
throw newOperationCanceledException();
return resultSelector(source.Result, inner);
}
public static asyncTaskCast (thisTask source)
{
await source;
return (T)((dynamic)source).Result;
}
Interestingly, Task
public T Extract
(thisW self);
publicW Extend(thisW self, Func<W , U> func);
Task
public T Result;
And it already supports Extend, it’s just called ContinueWith:
publicTask
ContinueWith (
Func<Task, TNewResult> func);
(In truth, to correctly implement all of the comonadic laws Brian outlines, we’d likely want to tweak both of these with a thin layer of additional code to modify some corner cases around exceptions and cancellation, due to how Result propagates exceptions wrapped in AggregateException and how ContinueWith tries to match thrown OperationCanceledExceptions against the CancellationToken supplied to ContinueWith. But the basic idea stands.)
Most of the posts I write on this blog are about practical things. So, is any of this really useful in everyday coding with Tasks and async/await? Would you actually want to implement and use the LINQ surface area directly for Tasks? Eh, probably not. But it’s a fun to see how all of these things relate.