在C#中通过异步/等待来“也许” monad(无任务!)


通用异步返回类型 -这是C#7的一项新功能,不仅允许将Task作为异步方法的返回类型使用,还可以使用满足某些特定要求的其他类型(类或结构)。


同时, async / await是在某些上下文中调用一组“ continuation”函数的方法,这是另一种设计模式Monad的本质。 因此,我们可以使用async / await编写代码的行为与使用monad时相同吗? 事实证明-是的(有些保留)。 例如,下面的代码是可编译的并且可以正常工作:


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);//No result checking var result = 0; foreach (var arg in args) result += await Parse(arg);//No result checking 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(); 

此外,我将解释代码的工作原理...


广义异步返回类型


首先,让我们弄清楚使用我们自己的类型(例如MyAwaitable <T> )作为某些异步函数的结果类型需要什么。 文档说这种类型必须具有:


  1. GetAwaiter()方法,它返回一个实现INotifyCompletion接口并具有bool IsCompleted属性和T GetResult()方法的类型的对象;


  2. [AsyncMethodBuilder(Type)]属性指向“方法生成器”类( 或结构 ),例如,使用以下方法的MyAwaitableTaskMethodBuilder <T>


    • 静态Create()
    • 开始(stateMachine)
    • SetResult(结果)
    • SetException(异常)
    • SetStateMachine(stateMachine)
    • AwaitOnCompleted(awaiter,stateMachine)
    • AwaitUnsafeOnCompleted(等待者,stateMachine)
    • 工作任务


这是MyAwaitable和MyAwaitableTaskMethodBuilder的简单实现
 [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; } } 

现在我们可以将MyAwaitable用作异步方法的结果类型:


 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);//Simulate asynchronous execution return await new MyAwaitable<int>(arg); } 

该代码可以正常工作,但是要了解MyAwaitable需求的目的,让我们看一下C#预处理程序对MyAwaitableMethod所做的工作 。 如果您运行一些反编译工具(例如dotPeek),则会看到原始方法已更改如下:


 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; } 

MyAwaitableMethodStateMachine

实际上,这是一个简化的代码,在这里我省略了很多优化以使编译器生成的代码可读


 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 should be completed 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 should be completed 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 should be completed 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); } } 

查看生成的代码,我们可以看到“方法生成器”具有以下职责:


  1. 在完成子异步操作后安排状态机的MoveNext()方法调用(在最简单的情况下,我们只需将MoveNext()传递到异步操作等待者的OnCompleted()中)。
  2. 创建异步操作上下文对象( public MyAwaitable<T> Task { get; }
  3. 对生成的状态机的最终状态做出反应: SetResultSetException

换句话说,通过“方法构建器”,我们可以控制异步方法的执行方式,并且它看起来像一项有助于我们实现目标的功能-Maybe monad行为的实现。 但是那个单子有什么好处呢? 好吧...您可以在Internet上找到很多有关该monad的文章,因此在这里我仅介绍基本知识。


也许单子


简而言之, 也许 monad是一种设计模式,如果该函数调用链中的某些函数无法产生有价值的结果(例如,解析错误),则该模式允许中断该函数调用链。


从历史上看,命令式编程语言一直通过两种方式解决该问题:


  1. 很多条件逻辑
  2. 例外情况

两种方法都有明显的缺点,因此发明了第三种方法:


  1. 创建一个可以有2种状态的类型:“某些值”和“无”-我们称其为“可能”
  2. 创建一个函数(让我们将其称为“ SelectMany”)以检索2个参数:
    2.1。 “也许”类型的对象
    2.2。 调用集中的下一个函数-该函数还应该返回一个“ Maybe”对象,如果无法评估其结果(例如,函数参数的格式不正确),则该对象将包含结果或“ Nothing”
  3. “ SelectMany”函数检查“ Maybe”是否具有某个值,然后使用该值(从“ Maybe”中提取)作为参数调用下一个函数,然后返回其结果,否则返回“ Nothing”状态的“ Maybe”对象。


在C#中,可以这样实现:


 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()); } 

和用法:


 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; } 

为什么选择“ SelectMany”?

我认为有些人可能会问一个问题:“作者为什么要调用函数“ SelectMany”?这真是个奇怪的名字”。 是的,但这是有原因的。 在C#中,预处理器使用SelectMany支持查询表示法 ,从而简化了调用链的使用(您可以在上一篇文章中找到更多详细信息 )。


实际上,我们可以如下更改呼叫链:


 var res = Function1(i) .SelectMany(x2 => Function2(x2).SelectMany(x3 => Function3(x3.SelectMany<int, int>(x4 => x2 + x3 + x4))); 

这样我们就可以访问所有中间结果,这很方便,但是代码很难阅读。


这里的查询符号可以帮助我们:


 var res = from x2 in Function1(i) from x3 in Function2(x2) from x4 in Function3(x3) select x2 + x3 + x4; 

为了使代码可编译,我们需要增强的“选择很多”版本


 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)); //Recursion is used to process a list of "Maybes" 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; 

由于C#不是作为一种功能语言设计的,所以代码看起来不太好,但是在像Haskell这样的“真正”功能语言中,这种方法非常普遍


异步也许


Maybe monad的本质是控制函数调用链,但这正是“异步/等待”的作用。 因此,让我们尝试将它们组合在一起。 首先,我们需要使Maybe类型与异步函数兼容,并且我们已经知道如何做到这一点:


 [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() =>... } 

现在,让我们看一下如何将“经典的Maybe”重写为状态机,以发现任何相似之处:


 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; } } } 

如果我们将此状态机与C#预处理程序之一生成的状态机匹配(请参见上文-'MyAwaitableMethodStateMachine'),我们会注意到也许可以在内部实现状态检查:


 this.Builder.AwaitOnCompleted(ref awaiter, ref stateMachine); 

其中ref awaiterMaybe类型的对象。 唯一的问题是我们无法将机器设置为“最终”(-1)状态。 这是否意味着我们无法控制执行流程? 实际上,事实并非如此。 事实是,对于每个异步动作,C#都会通过INotifyCompletion接口设置一个回调动作,因此,如果我们想中断执行流程,我们可以在无法继续执行流程的情况下调用该回调动作。
这里的另一个挑战是生成的状态机传递当前流的下一个动作(作为继续回调),但是我们需要初始流的继续回调,这将允许绕过其余的异步操作:



因此,我们需要以某种方式将子异步操作与其祖先联系起来。 我们可以使用我们的“方法生成器”来做到这一点,该方法链接到当前的异步操作-Task 。 所有子异步操作的链接都将传递给AwaitOnCompleted(ref awaiter称为awaiter ,所以我们只需要检查该参数是否为Maybe的实例,然后将当前Maybe设置为该子代的父代即可:


 [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); } ... } 

现在,所有Maybe对象都可以连接到树中,因此,我们可以从任何后代节点访问根Maybe的延续( Exit方法):


 [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(); } } ... } 

当(在移动树上时)我们发现一个已经解析的Maybe对象处于Nothing状态时,应调用Exit方法。 这样的Maybe对象可以通过如下方法返回:


 Maybe<int> Parse(string str) => int.TryParse(str, out var result) ? result : Maybe<int>.Nothing(); 

为了存储已解决状态, 也许我们引入一个新的单独结构:


 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;// "Resolved" instance ... } 

当异步状态机(通过方法构建器)调用已解析的Maybe实例的OnCompleted方法并且它处于Nothing状态时,我们将能够中断整个流程:


 public void OnCompleted(Action continuation) { this._continuation = continuation; if(this._result.HasValue) { this.NotifyResult(this._result.Value.IsNothing); } } internal void SetResult(T result) //Is called by a "method builder" when an async method is completed { 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();//Braking an entire flow } else { this._continuation?.Invoke(); } } 

现在唯一剩下的就是-如何获得可能超出其范围的异步Maybe结果(任何返回类型不是Maybe的异步方法)。 如果您尝试将Maya实例仅使用await关键字,则由于以下代码将引发异常:


 [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; } 

为了解决这个问题,我们只需要添加一个新的Awaiter即可返回完整的MaybeResult结构,我们将可以编写如下代码:


 var res = await GetResult().GetMaybeResult(); if(res.IsNothing){ ... } else{ res.GetValue(); ... }; 

现在就这些了。 在代码示例中,我忽略了一些细节,仅将重点放在最重要的部分上。 您可以在github上找到完整版本。


但是 ,我不建议在任何生产代码中使用此版本,因为它存在严重的问题-当我们通过调用root的延续来制动执行流时, 也许我们将绕过一切! 包括所有的finally块(这是对“是否总是调用finally块?”这个问题的回答),因此所有使用运算符的操作将无法按预期工作,并且可能导致资源泄漏。 如果我们不是抛出初始的继续回调,而是抛出一个内部处理的特殊异常( 可以在此处找到版本 ),则可以解决此问题,但是此解决方案显然具有性能上的模仿(在某些情况下可以接受)。 使用当前版本的C#编译器,我看不到任何其他解决方案,但将来可能会更改。


这些限制并不意味着本文中描述的所有技巧都是完全没有用的,它们可用于实现不需要更改执行流程的其他monad,例如“ Reader”。 我将在下一篇文章中介绍如何通过异步/等待来实现“阅读器” monad。

Source: https://habr.com/ru/post/zh-CN458692/


All Articles