Generic asynchronous return types are a new feature introduced in C # 7 that allows you to use not only Task as the return type of asynchronous ( async / await ) methods, but also any other types (classes or structures) that satisfy certain requirements.
At the same time, async / await is a way to consistently call a certain set of functions within a certain context, which is the essence of the Monad design pattern. The question is, can we use async / await to write code that behaves as if we were using monads? It turns out that yes (with some reservations). For example, the code below compiles and works:
async Task Main() { foreach (var s in new[] { "1,2", "3,7,1", null, "1" }) { var res = await Sum(s).GetMaybeResult(); Console.WriteLine(res.IsNothing ? "Nothing" : res.GetValue().ToString()); } // 3, 11, Nothing, Nothing } async Maybe<int> Sum(string input) { var args = await Split(input);// var result = 0; foreach (var arg in args) result += await Parse(arg);// return result; } Maybe<string[]> Split(string str) { var parts = str?.Split(',').Where(s=>!string.IsNullOrWhiteSpace(s)).ToArray(); return parts == null || parts.Length < 2 ? Maybe<string[]>.Nothing() : parts; } Maybe<int> Parse(string str) => int.TryParse(str, out var result) ? result : Maybe<int>.Nothing();
Next, I explain how this code works ...
First of all, let's find out what is required to use our own type (for example, the MyAwaitable <T> class) as the result type of some asynchronous function. The documentation says that this type should have:
GetAwaiter () method that returns an object of type that implements the INotifyCompletion interface and also has the bool IsCompleted property and the T method GetResult () ;
[AsyncMethodBuilder (Type)] - an attribute indicating the type that will act as the " Method Builder ", for example MyAwaitableTaskMethodBuilder <T> . This type should contain in the following methods:
[AsyncMethodBuilder(typeof(MyAwaitableTaskMethodBuilder<>))] public class MyAwaitable<T> : INotifyCompletion { private Action _continuation; public MyAwaitable() { } public MyAwaitable(T value) { this.Value = value; this.IsCompleted = true; } public MyAwaitable<T> GetAwaiter() => this; public bool IsCompleted { get; private set; } public T Value { get; private set; } public Exception Exception { get; private set; } public T GetResult() { if (!this.IsCompleted) throw new Exception("Not completed"); if (this.Exception != null) { ExceptionDispatchInfo.Throw(this.Exception); } return this.Value; } internal void SetResult(T value) { if (this.IsCompleted) throw new Exception("Already completed"); this.Value = value; this.IsCompleted = true; this._continuation?.Invoke(); } internal void SetException(Exception exception) { this.IsCompleted = true; this.Exception = exception; } void INotifyCompletion.OnCompleted(Action continuation) { this._continuation = continuation; if (this.IsCompleted) { continuation(); } } } public class MyAwaitableTaskMethodBuilder<T> { public MyAwaitableTaskMethodBuilder() => this.Task = new MyAwaitable<T>(); public static MyAwaitableTaskMethodBuilder<T> Create() => new MyAwaitableTaskMethodBuilder<T>(); public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => stateMachine.MoveNext(); public void SetStateMachine(IAsyncStateMachine stateMachine) { } public void SetException(Exception exception) => this.Task.SetException(exception); public void SetResult(T result) => this.Task.SetResult(result); public void AwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine => this.GenericAwaitOnCompleted(ref awaiter, ref stateMachine); public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine => this.GenericAwaitOnCompleted(ref awaiter, ref stateMachine); public void GenericAwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine => awaiter.OnCompleted(stateMachine.MoveNext); public MyAwaitable<T> Task { get; } }
Now we can use MyAwaitable as the result type of asynchronous methods:
private async MyAwaitable<int> MyAwaitableMethod() { int result = 0; int arg1 = await this.GetMyAwaitable(1); result += arg1; int arg2 = await this.GetMyAwaitable(2); result += arg2; int arg3 = await this.GetMyAwaitable(3); result += arg3; return result; } private async MyAwaitable<int> GetMyAwaitable(int arg) { await Task.Delay(1);// return await new MyAwaitable<int>(arg); }
This code works, but to understand the essence of the requirements for the MyAwaitable class , let's see what the C # preprocessor does with the MyAwaitableMethod method. If you run some .NET compiler decompiler (for example, dotPeek), you will see that the original method has been changed as follows:
private MyAwaitable<int> MyAwaitableMethod() { var stateMachine = new MyAwaitableMethodStateMachine(); stateMachine.Owner = this; stateMachine.Builder = MyAwaitableTaskMethodBuilder<int>.Create(); stateMachine.State = 0; stateMachine.Builder.Start(ref stateMachine); return stateMachine.Builder.Task; }
This is actually simplified code, where I skip a lot of optimizations to make the code generated by the compiler readable
sealed class MyAwaitableMethodStateMachine : IAsyncStateMachine { public int State; public MyAwaitableTaskMethodBuilder<int> Builder; public BuilderDemo Owner; private int _result; private int _arg1; private int _arg2; private int _arg3; private MyAwaitableAwaiter<int> _awaiter1; private MyAwaitableAwaiter<int> _awaiter2; private MyAwaitableAwaiter<int> _awaiter3; private void SetAwaitCompletion(INotifyCompletion awaiter) { var stateMachine = this; this.Builder.AwaitOnCompleted(ref awaiter, ref stateMachine); } void IAsyncStateMachine.MoveNext() { int finalResult; try { label_begin: switch (this.State) { case 0: this._result = 0; this._awaiter1 = this.Owner.GetMyAwaitable(1).GetAwaiter(); this.State = 1; if (!this._awaiter1.IsCompleted) { this.SetAwaitCompletion(this._awaiter1); return; } goto label_begin; case 1:// awaiter1 this._arg1 = this._awaiter1.GetResult(); this._result += this._arg1; this.State = 2; this._awaiter2 = this.Owner.GetMyAwaitable(2).GetAwaiter(); if (!this._awaiter2.IsCompleted) { this.SetAwaitCompletion(this._awaiter2); return; } goto label_begin; case 2:// awaiter2 this._arg2 = this._awaiter2.GetResult(); this._result += this._arg2; this.State = 3; this._awaiter3 = this.Owner.GetMyAwaitable(3).GetAwaiter(); if (!this._awaiter3.IsCompleted) { this.SetAwaitCompletion(this._awaiter3); return; } goto label_begin; case 3:// awaiter3 this._arg3 = this._awaiter3.GetResult(); this._result += this._arg3; finalResult = this._result; break; default: throw new Exception(); } } catch (Exception ex) { this.State = -1; this.Builder.SetException(ex); return; } this.State = -1; this.Builder.SetResult(finalResult); } }
After examining the generated code, we see that the Method Builder has the following responsibilities:
public MyAwaitable<T> Task { get; }
In other words, with the help of Method Builder we can gain control over how asynchronous methods are executed, and this looks like an opportunity that will help us achieve our goal - the implementation of Maybe monad behavior.
But what is so good about this monad? ... In fact, you can find many articles about this monad on the Internet, so here I will describe only the basics.
In short, Maybe monad is a design pattern that allows you to interrupt the chain of function calls if some function from the chain cannot return a meaningful result (for example, invalid input parameters).
Historically imperative programming languages ββhave solved this problem in two ways:
Both methods have obvious disadvantages, so an alternative approach was proposed:
In C #, this can be implemented as follows:
public struct Maybe<T> { public static implicit operator Maybe<T>(T value) => Value(value); public static Maybe<T> Value(T value) => new Maybe<T>(false, value); public static readonly Maybe<T> Nothing = new Maybe<T>(true, default); private Maybe(bool isNothing, T value) { this.IsNothing = isNothing; this._value = value; } public readonly bool IsNothing; private readonly T _value; public T GetValue() => this.IsNothing ? throw new Exception("Nothing") : this._value; } public static class MaybeExtensions { public static Maybe<TRes> SelectMany<TIn, TRes>( this Maybe<TIn> source, Func<TIn, Maybe<TRes>> func) => source.IsNothing ? Maybe<TRes>.Nothing : func(source.GetValue()); }
and usage example:
static void Main() { for (int i = 0; i < 10; i++) { var res = Function1(i).SelectMany(Function2).SelectMany(Function3); Console.WriteLine(res.IsNothing ? "Nothing" : res.GetValue().ToString()); } Maybe<int> Function1(int acc) => acc < 10 ? acc + 1 : Maybe<int>.Nothing; Maybe<int> Function2(int acc) => acc < 10 ? acc + 2 : Maybe<int>.Nothing; Maybe<int> Function3(int acc) => acc < 10 ? acc + 3 : Maybe<int>.Nothing; }
I think some of you may wonder: βWhy did the author call this functionβ SelectMany β? Actually, there is a reason for this - in C # the preprocessor inserts a Select Many call when processing expressions written in Query Notation , which, in essence, is βSyntactic sugarβ for complex chains of calls. (You can find more information about this in my previous article ).
In fact, we can rewrite the previous code as follows:
var res = Function1(i) .SelectMany(x2 => Function2(x2).SelectMany(x3 => Function3(x3.SelectMany<int, int>(x4 => x2 + x3 + x4)));
thus gaining access to the intermediate state (x2, x3), which in some cases can be very convenient. Unfortunately, reading such code is very difficult, but fortunately, C # has a Query Notation with the help of which such code will look much easier:
var res = from x2 in Function1(i) from x3 in Function2(x2) from x4 in Function3(x3) select x2 + x3 + x4;
In order to make this code compiled, we need to slightly expand the Select Many function:
public static Maybe<TJ> SelectMany<TIn, TRes, TJ>( this Maybe<TIn> source, Func<TIn, Maybe<TRes>> func, Func<TIn, TRes, TJ> joinFunc) { if (source.IsNothing) return Maybe<TJ>.Nothing; var res = func(source.GetValue()); return res.IsNothing ? Maybe<TJ>.Nothing : joinFunc(source.GetValue(), res.GetValue()); }
static void Main() { foreach (var s in new[] {"1,2", "3,7,1", null, "1"}) { var res = Sum(s); Console.WriteLine(res.IsNothing ? "Nothing" : res.GetValue().ToString()); } Console.ReadKey(); } static Maybe<int> Sum(string input) => Split(input).SelectMany(items => Acc(0, 0, items)); // "Maybe" static Maybe<int> Acc(int res, int index, IReadOnlyList<string> array) => index < array.Count ? Add(res, array[index]) .SelectMany(newRes => Acc(newRes, index + 1, array)) : res; static Maybe<int> Add(int acc, string nextStr) => Parse(nextStr).SelectMany<int, int>(nextNum => acc + nextNum); static Maybe<string[]> Split(string str) { var parts = str?.Split(',') .Where(s => !string.IsNullOrWhiteSpace(s)).ToArray(); return parts == null || parts.Length < 2 ? Maybe<string[]>.Nothing : parts; } static Maybe<int> Parse(string value) => int.TryParse(value, out var result) ? result : Maybe<int>.Nothing;
This code does not look very elegant, since C # was not originally designed as a functional language, but in βrealβ functional languages, this approach is very common.
The essence of Maybe monad is to control the chain of function calls, but this is exactly what async / await does. So, let's try to combine them together. First, we need to make the Maybe type compatible with asynchronous functions, and we already know how to achieve this:
[AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : INotifyCompletion { ... public Maybe<T> GetAwaiter() => this; public bool IsCompleted { get; private set; } public void OnCompleted(Action continuation){...} public T GetResult() =>... }
Now let's see how the βclassicβ Maybe implementation can be rewritten as a state machine so that we can find any similarities:
static void Main() { for (int i = 0; i < 10; i++) { var stateMachine = new StateMachine(); stateMachine.state = 0; stateMachine.i = i; stateMachine.MoveNext(); var res = stateMachine.Result; Console.WriteLine(res.IsNothing ? "Nothing" : res.GetValue().ToString()); } Console.ReadKey(); } class StateMachine { public int state = 0; public int i; public Maybe<int> Result; private Maybe<int> _f1; private Maybe<int> _f2; private Maybe<int> _f3; public void MoveNext() { label_begin: switch (this.state) { case 0: this._f1 = Function1(this.i); this.state = Match ? -1 : 1; goto label_begin; case 1: this._f2 = Function2(this._f1.GetValue()); this.state = this._f2.IsNothing ? -1 : 2; goto label_begin; case 2: this._f3 = Function3(this._f2.GetValue()); this.state = this._f3.IsNothing ? -1 : 3; goto label_begin; case 3: this.Result = this._f3.GetValue(); break; case -1: this.Result = Maybe<int>.Nothing; break; } } }
If we compare this state machine with the generated C # preprocessor (see "MyAwaitableMethodStateMachine" above), we can notice that Maybe status checking can be implemented inside:
this.Builder.AwaitOnCompleted(ref awaiter, ref stateMachine);
where ref awaiter
is an object of type Maybe . The problem here is that we cannot set the machine to the βfinalβ (-1) state, but does this mean that we cannot control the flow of execution? This is actually not the case. The fact is that for each asynchronous action, C # sets up a callback function to continue the asynchronous action through the INotifyCompletion interface, so if we want to break the flow of execution, we can simply call the callback function when we cannot continue the chain of asynchronous operations.
Another problem here is that the generated state machine transfers the next step (as a callback function) to the current sequence of asynchronous operations, but we need a callback function for the original sequence that would allow us to bypass all the remaining chains of asynchronous operations (from any nesting level) :
So, we need to somehow associate the current nested asynchronous action with its creator. We can do this using our Method Builder , which has a link to the current asynchronous operation - Task . Links to all child asynchronous operations will be passed to AwaitOnCompleted (ref awaiter)
as awaiter , so we just need to check if the parameter is an instance of Maybe , and then set the current Maybe as the parent for the current child action:
[AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : IMaybe, INotifyCompletion { private IMaybe _parent; void IMaybe.SetParent(IMaybe parent) => this._parent = parent; ... } public class MaybeTaskMethodBuilder<T> { ... private void GenericAwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { if (awaiter is IMaybe maybe) { maybe.SetParent(this.Task); } awaiter.OnCompleted(stateMachine.MoveNext); } ... }
Now all objects of type Maybe can be combined into a hierarchy, as a result of which, we will get access to the final call of the entire hierarchy ( Exit method) from any node:
[AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : IMaybe, INotifyCompletion { private Action _continuation; private IMaybe _parent; ... public void OnCompleted(Action continuation) { ... this._continuation = continuation; ... } ... void IMaybe.Exit() { this.IsCompleted = true; if (this._parent != null) { this._parent.Exit(); } else { this._continuation(); } } ... }
The Exit method should be called when, while navigating through the hierarchy, we found the already computed Maybe object in the Nothing state. Such Maybe objects can be returned by methods like this:
Maybe<int> Parse(string str) => int.TryParse(str, out var result) ? result : Maybe<int>.Nothing();
To store the Maybe state, create a new separate structure:
public struct MaybeResult { ... private readonly T _value; public readonly bool IsNothing; public T GetValue() => this.IsNothing ? throw new Exception("Nothing") : this._value; } [AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : IMaybe, INotifyCompletion { private MaybeResult? _result; ... internal Maybe() { }//Used in async method private Maybe(MaybeResult result) => this._result = result;// "" ... }
At the moment when the asynchronous state machine calls (via Method Builder ) the OnCompleted method of the already calculated Maybe instance and it is in the Nothing state, we can break the entire stream:
public void OnCompleted(Action continuation) { this._continuation = continuation; if(this._result.HasValue) { this.NotifyResult(this._result.Value.IsNothing); } } internal void SetResult(T result) // "method builder" { this._result = MaybeResult.Value(result); this.IsCompleted = true; this.NotifyResult(this._result.Value.IsNothing); } private void NotifyResult(bool isNothing) { this.IsCompleted = true; if (isNothing) { this._parent.Exit();// } else { this._continuation?.Invoke(); } }
Now there remains only one question - how to get the result of the asynchronous Maybe outside its scope (any asynchronous method whose return type is not Maybe ). If you try to use only the await keyword with the Maybe instance, then an exception will be thrown by this code:
[AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : IMaybe, INotifyCompletion { private MaybeResult? _result; public T GetResult() => this._result.Value.GetValue(); } ... public struct MaybeResult { ... public T GetValue() => this.IsNothing ? throw new Exception("Nothing") : this._value; }
To solve this problem, we can simply add a new awaiter that will return the entire MaybeResult structure as a whole, and then we can write this code:
var res = await GetResult().GetMaybeResult(); if(res.IsNothing){ ... } else{ res.GetValue(); ... };
That's all for now. In the code examples, I omitted some details to focus only on the most important parts. You can find the full version on github .
In fact , I would not recommend using the above approach in any working code, since it has one significant problem - when we break the thread of execution, causing the continuation of the root asynchronous operation (with type Maybe ), we break ALL at all! including all finally blocks (this is the answer to the question βAre finally blocks always called?β), so all using statements will not work properly, which could result in a resource leak. This problem can be solved if instead of directly calling the continuation, we will raise a special exception that will be implicitly handled ( here you can find this version ), but this solution obviously has a performance limit (which may be acceptable in some scenarios). In the current version of the C # compiler, I do not see another solution, but maybe this will someday change in the future.
However, these restrictions do not mean that all the techniques described in this article are completely useless, they can be used to implement other monads that do not require changes in the threads, for example, "Reader". How to implement this "Reader" monad through async / await I will show in the next article .