Maybe Monad通过C#中的异步/等待(没有Task ov!)


通用异步返回类型是C#7中引入的一项新功能,它使您不仅可以使用Task作为异步( async / await )方法的返回类型,还可以使用满足某些要求的任何其他类型(类或结构)。


同时, 异步/等待是一种在特定上下文中顺序调用特定功能集的方法,这是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);//   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(); 

接下来,我解释一下此代码的工作方式...


通用异步返回类型


首先,让我们找出使用我们自己的类型(例如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);//   return await new MyAwaitable<int>(arg); } 

这段代码有效,但是要了解MyAwaitable类的要求的本质让我们看看C#预处理程序对MyAwaitableMethod方法的作用。 如果运行某些.NET编译器反编译器(例如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    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); } } 

检查生成的代码后,我们看到方法构建器具有以下职责:


  1. 调用MoveNext()方法的组织,该方法将生成的状态机转移到下一个状态。
  2. 创建一个表示异步操作上下文的对象( public MyAwaitable<T> Task { get; }
  3. 响应于将生成的状态机转换为最终状态: SetResultSetException

换句话说,借助Method Builder,我们可以控制异步方法的执行方式,这看起来像是一个机会,可以帮助我们实现目标-实现Maybe monad行为。


但是,这个monad有什么好处呢?...实际上,您可以在Internet上找到许多有关此monad的文章,因此在这里我仅介绍基本知识。


也许莫纳德


简而言之, 也许 monad是一种设计模式,如果链中的某个函数无法返回有意义的结果(例如,无效的输入参数),则可以中断该函数调用链。


从历史上讲,命令式编程语言已通过两种方式解决了此问题:


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

两种方法都有明显的缺点,因此提出了一种替代方法:


  1. 创建一个可以处于两种状态的类型:“某些值”和“无值”(“ ”)-我们称之为Maybe
  2. 创建一个带有2个参数的函数(我们称之为SelectMany ):
    2.1。 也许是对象
    2.2。 呼叫列表中的下一个功能。 此函数还应该返回Maybe类型的对象,该对象可能包含某种结果值,或者如果无法获得结果(例如,错误的参数已传递给该函数),则处于Nothing状态。
  3. SelectMany函数检查Maybe类型的对象,如果它包含结果值,则将提取此结果并将其作为参数传递给调用链中的下一个函数(作为第二个参数传递)。 如果Maybe对象处于Nothing状态,则SelectMany将立即返回Nothing


在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#中,预处理器在处理以Query Notation编写的表达式时会插入Select Many调用,从本质上讲,这是“语法糖”用于复杂的呼叫链(您可以在我的上一篇文章中找到有关此信息的更多信息 )。


实际上,我们可以按如下方式重写前面的代码:


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

因此可以访问中间状态(x2,x3),这在某些情况下可能非常方便。 不幸的是,阅读这样的代码非常困难,但是幸运的是,C#拥有一个查询符号 ,借助它,这样的代码看起来会容易得多:


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

为了使此代码得以编译,我们需要稍微扩展Select Many函数:


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

如果您使用“经典” Maybe实现重写文章,则标题中的代码将是这样的
 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; 

由于C#最初并不是设计为功能语言,所以此代码看起来并不优雅,但是这种方法在“实际”功能语言中非常普遍。


异步也许


Maybe monad的本质是控制函数调用链,但这正是async / await所做的。 因此,让我们尝试将它们组合在一起。 首先,我们需要使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的链接。 指向所有子异步操作的链接将作为awaiter传递给AwaitOnCompleted (ref 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类型的所有对象组合到一个层次结构中,结果是,我们可以从任何节点访问整个层次结构的最终调用( 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(); 

要存储Maybe状态,请创建一个新的单独结构:


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

当异步状态机调用(通过Method Builder )已计算的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) //  "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(); } } 

现在仅存在一个问题-如何获取异步Maybe的结果超出其范围(任何返回类型不是Maybe的异步方法)。 如果尝试对Maybe实例仅使用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; } 

为了解决这个问题,我们可以简单地添加一个新的waiter ,它将整体返回整个MaybeResult结构,然后可以编写以下代码:


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

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


实际上 ,我不建议在任何工作代码中使用上述方法,因为它存在一个重大问题-当我们中断执行线程,导致继续执行根异步操作(类型为Maybe )时,我们将完全中断所有! 包括所有的finally块(这是“是否总是调用finally块?”这个问题的答案),因此所有using语句将无法正常工作,这可能导致资源泄漏。 如果我们不是引发直接调用延续,而是引发将被隐式处理的特殊异常( 在这里您可以找到此版本 ),则可以解决此问题,但是此解决方案显然具有性能限制(在某些情况下可以接受)。 在当前版本的C#编译器中,我看不到其他解决方案,但是也许将来有一天会有所改变。


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

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


All Articles