"ربما" أحادي خلال المزامنة / في انتظار C # (بلا مهام!)


أنواع الإرجاع المتزامن العامة - إنها ميزة C # 7 جديدة تسمح باستخدام ليس فقط Task كنوع إرجاع لأساليب غير متزامنة ولكن أيضًا أنواع أخرى (فئات أو بنيات) تلبي بعض المتطلبات المحددة.


في الوقت نفسه ، يمثل المزامنة / الانتظار طريقة لاستدعاء مجموعة من وظائف "الاستمرارية" داخل بعض السياق الذي يعد جوهرًا لنمط تصميم آخر - Monad . لذا ، هل يمكننا استخدام المزامنة / الانتظار لكتابة رمز يتصرف بنفس الطريقة كما لو أننا استخدمنا monads؟ اتضح ذلك - نعم (مع بعض التحفظات). على سبيل المثال ، الشفرة أدناه قابلة للترجمة وتعمل:


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 ولديه خاصية IsCompleted منطقية وطريقة T GetResult () ؛


  2. سمة [AsyncMethodBuilder (Type)] التي تشير إلى فئة ( أو بنية ) "منشئ الطريقة" (مثال: MyAwaitableTaskMethodBuilder <T> بالطرق التالية:


    • إنشاء ثابت ()
    • البداية (ولاية ماشين)
    • SetResult (النتيجة)
    • SetException (استثناء)
    • SetStateMachine (stateMachine)
    • AwaitOnCompleted (awaiter ، stateMachine)
    • AwaitUnsafeOnCompleted (awaiter، 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 . إذا قمت بتشغيل بعض de-compilation (مثل 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. الرد على الحالات النهائية لآلات الحالة التي تم إنشاؤها: SetResult أو SetException .

بمعنى آخر ، من خلال "مُنشئي الأساليب" ، يمكننا التحكم في كيفية تنفيذ الأساليب غير المتزامنة ويبدو أنها ميزة ستساعدنا على تحقيق هدفنا - تنفيذ سلوك ربما أحادي. ولكن ما هو جيد عن هذا monad؟ حسنًا ... يمكنك العثور على الكثير من المقالات حول هذا الموناد في الإنترنت ، لذلك سوف أصف هنا الأساسيات فقط.


ربما أحادي


باختصار ، ربما يكون monad عبارة عن نمط تصميم يتيح مقاطعة سلسلة استدعاء دالة إذا لم تتمكن بعض الوظائف من السلسلة من الحصول على نتيجة قيمة (مثل تحليل الخطأ).


تاريخيا ، كانت لغات البرمجة الملحة تحل المشكلة بطريقتين:


  1. الكثير من المنطق الشرطي
  2. استثناءات

لكلتا الطريقتين عيوب واضحة ، لذلك اخترع طريق ثالث:


  1. قم بإنشاء نوع يمكن أن يكون في حالتين: "بعض القيمة" و "لا شيء" - دعنا نسميها "ربما"
  2. أنشئ وظيفة (دعنا نسميها "SelectMany") تسترجع وسيطين:
    2.1. كائن من نوع "ربما"
    2.2. وظيفة تالية من مجموعة الاتصال - يجب أن تُرجع الدالة أيضًا كائنًا "ربما" سيحتوي على نتيجة أو "لا شيء" إذا تعذر تقييم نتيجتها (على سبيل المثال ، معلمات الوظيفة ليست بالتنسيق الصحيح)
  3. تقوم وظيفة "SelectMany" بالتحقق مما إذا كانت "ربما" لها قيمة ثم تستدعي الوظيفة التالية باستخدام القيمة (المستخرجة من "ربما") كوسيطة ثم ترجع النتيجة ، وإلا فإنها تُرجع كائن "ربما" في حالة "لا شيء" .


في 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 ، يعد هذا النهج شائعًا للغاية


متزامن ربما


إن جوهر ربما monad هو التحكم في سلسلة نداء الوظائف ، ولكن هذا بالضبط هو "async / انتظار". لذلك دعونا نحاول الجمع بينهما. أولاً ، نحتاج إلى جعل ربما تكتب متوافقًا مع وظائف غير متزامنة ونعرف بالفعل كيفية القيام بذلك:


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

الآن دعونا نلقي نظرة على كيفية إعادة كتابة "ربما الكلاسيكية" كجهاز الدولة لتكون قادرة على العثور على أي أوجه التشابه:


 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 awaiter هو كائن من نوع ربما . المشكلة الوحيدة هنا هي أننا لا نستطيع ضبط الجهاز في حالة "النهائية" (-1). هل هذا يعني أننا لا نستطيع التحكم في تدفق التنفيذ؟ في الواقع ، لا. الشيء هو أنه بالنسبة لكل إجراء غير متزامن ، تقوم C # بتعيين إجراء رد اتصال من خلال واجهة INotifyCompletion ، لذلك إذا أردنا كسر تدفق التنفيذ ، يمكننا فقط استدعاء إجراء رد الاتصال في حالة عندما يتعذر علينا مواصلة التدفق.
التحدي الآخر هنا هو أن جهاز الحالة المولدة يمرر الإجراء التالي (كرد اتصال مستمر) للتدفق الحالي ، لكننا نحتاج إلى رد اتصال مستمر للتدفق الأولي الذي يسمح بتجاوز باقي عمليات المزامنة:



لذلك ، نحن بحاجة إلى ربط عمل مزامنة الطفل بطريقة ما مع أسلافه. يمكننا القيام بذلك باستخدام "أداة إنشاء" الخاصة بنا والتي تحتوي على رابط لعملية مزامنة حالية - مهمة . سيتم تمرير الروابط إلى جميع عمليات AwaitOnCompleted(ref awaiter في AwaitOnCompleted(ref awaiter كمسؤول انتظار ، لذلك نحتاج فقط إلى التحقق مما إذا كانت المعلمة هي مثال ربما ، وإذا كان قد تم تعيينها الحالية ، فربما تكون AwaitOnCompleted(ref awaiter للطفل:


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

الآن يمكن ربط كل الكائنات ربما في شجرة ، ونتيجة لذلك ، سوف نحصل على استمرار للجذر ربما (طريقة الخروج ) من أي عقدة هبوطية:


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

عندما تستدعي آلة حالة غير متزامنة (من خلال منشئ الطريقة) طريقة OnCompleted لحل تم حله بالفعل ، ربما في حالة لا شيء ، سنكون قادرين على كسر تدفق كامل:


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

الآن يبقى الشيء الوحيد - كيفية الحصول على نتيجة لمزامنة ربما خارج نطاقه (أي طريقة غير متزامنة لن يكون نوع الإرجاع الخاص بها). إذا حاولت استخدام مجرد انتظار الكلمة الرئيسية مع مثال ربما ، فسيتم طرح استثناء بسبب هذا الرمز:


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

هذا كل شيء الآن. في نماذج التعليمات البرمجية ، أغفل بعض التفاصيل للتركيز فقط على الأجزاء الأكثر أهمية. يمكنك العثور على نسخة كاملة على جيثب .


ومع ذلك ، لا أوصي باستخدام هذا الإصدار في أي كود إنتاج لأنه يحتوي على مشكلة كبيرة - عندما نقضي على تدفق التنفيذ عن طريق استدعاء استمرار الجذر ربما سنتجاوز كل شيء! بما في ذلك جميع الكتل الأخيرة (إنها إجابة على السؤال "هل تسمى الكتل أخيرًا دائمًا؟") ، وبالتالي فإن جميع المشغلين الذين يستخدمون لن يعمل كما هو متوقع وقد يؤدي ذلك إلى تسرب الموارد. يمكن حل المشكلة إذا بدلاً من استدعاء رد الاتصال الأولي للاستمرار ، فسنطرح استثناءًا خاصًا سيتم معالجته داخليًا ( هنا يمكنك العثور على الإصدار ) ، ولكن يبدو أن هذا الحل يحتوي على تقليد للأداء (قد يكون مقبولًا في بعض السيناريوهات). مع الإصدار الحالي من برنامج التحويل البرمجي C # ، لا أرى أي حل آخر ولكن قد يتم تغيير ذلك في المستقبل.


لا تعني هذه القيود أن جميع الحيل الموضحة في هذه المقالة عديمة الفائدة تمامًا ، يمكن استخدامها لتنفيذ monads الأخرى التي لا تتطلب تغييرات في تدفقات التنفيذ ، على سبيل المثال "Reader". كيفية تنفيذ هذا "القارئ" أحادي الحوض الصغير / تنتظر سأظهر في المقالة التالية .

Source: https://habr.com/ru/post/ar458692/


All Articles