Linq Toタスクと継続モナド

非同期と待機を備えたC#5.0の発表後、非同期プログラミングに少し興味を持ち、その方向に掘り下げて、このテキストの元になりました。
記事を読む過程で、あなたは学びます/洗います。


モナド


モナドは基本的に3つの部分で構成されています。

通常、これら2つの関数はreturnおよびbindと呼ばれますが、シャープではTo [Monad Name]およびSelectManyと呼びます。

たぶんモナド

Nullableの双子の兄弟かもしれないモナドを考えてみましょう。 値を含むことも、何も含まないこともできるコンテナー。
public class Maybe<T> { public static readonly Maybe<T> Nothing = new Maybe<T>(); public T Value { get; private set; } public bool HasValue { get; private set; } private Maybe() { HasValue = false; } public Maybe(T value) { Value = value; HasValue = true; } public override string ToString() { if(HasValue) return Value.ToString(); return "Nothing"; } } 

このモナドの便利なプロパティの1つで、式のどこかにNothingが見つかった場合、式全体がNothingになります。
ここに2つの機能があります。
 public static class MaybeEx { public static Maybe<T> ToMaybe<T>(this T value) { return new Maybe<T>(value); } public static Maybe<U> SelectMany<T, U>(this Maybe<T> m, Func<T, Maybe<U>> k) { if (m.HasValue) return k(m.Value); return Maybe<U>.Nothing; } } 

ToMaybeではすべてが明確であり、SelectManyではコンテナmを取得し、値があればそのコンテンツに関数kを適用します。
2つの数字を追加するとします
 var a = 1.ToMaybe(); var b = Maybe<int>.Nothing; Func<int, Maybe<int>> k = x => b.SelectMany(y => (y + x).ToMaybe()); var result = a.SelectMany(k); //result == Nothing,   b = 2.ToMaybe()  3 

折りたたむ前に、SelectManyを使用してコンテナから値を2回引き出す必要があることがわかります。 ヒープ関数を取り除くために、一部の言語には砂糖があり、Haskellでは表記法、F#計算式、よく、式が
 from x in a from y in b select x + y; 

それは
 a.SelectMany(x => b, (x, y) => x + y); 

つまり、フォームの追加のSelectManyを実装することにより、
 public static Maybe<V> SelectMany<T, U, V>( this Maybe<T> m, Func<T, Maybe<U>> k, Func<T, U, V> s) { return m.SelectMany(x => k(x).SelectMany(y => s(x, y).ToMaybe())); } 

非常に二重のコンテナ拡張を含むだけで、フォームにMaybeの操作を記録できます。
 var result = from x in 1.ToMaybe() from y in 2.ToMaybe() select x + y; 


CPS(継続渡しスタイル)



CPSは基本的にコールバックで機能します。つまり、通常の計算順序を使用する代わりに
 var x = GetPage("http://habrahabr.ru"); var y = Translate(x); Display(y);// Display  Console.WriteLine 

各関数には追加のパラメーターがあり、計算を続行するために関数にパラメーターを渡します。
 GetPage("http://habrahabr.ru", x => Translate(x, y => Display(y))); 

関数がどこかで非同期リクエストを行い、スレッドをアイドル状態にしたり、GUIをフリーズさせたくない場合によく使用されます。
モナドとCPSを混合することにより、
続編モナド

モナドの多かれ少なかれ標準的なバージョンは、入力として別の関数をとる関数です。出力が無効である場合、単純な場合に限定します。 そうすれば、モナドはそのようなデリゲートとして表すことができます。
 public delegate void Cont<T>(Action<T> k); 

関数k(続き)は入力関数に提供され、T。のようなものが既に転送されています。
 public static class ContEx { public static Cont<T> ToCont<T>(this T value) { return k => k(value); } public static Cont<T1> SelectMany<T, T1>(this Cont<T> c, Func<T, Cont<T1>> f) { return k => c(r => f(r )(k)); } public static Cont<T2> SelectMany<T, T1, T2>(this Cont<T> c, Func<T, Cont<T1>> f, Func<T, T1, T2> s) { return c.SelectMany(x => f(x).SelectMany(y => s(x, y).ToCont())); } } 

ToCont-値を渡すだけでkを続行します。 SelectMany-モナドからrを取得し、それを関数fに渡し、共通の継続kで新しいモナドを呼び出します。

タスク

Taskが何であるかを思い出します。 これは、アクションを含むコンテナであり、その結果は、ストリームをブロックすることによりResultプロパティを使用して取得でき、CPSではContinueWithメソッドを使用します。
タスクを作成します。
 var task = Task<int>.Factory.StartNew(() => 10); //  10 

結果を得る
 task.ContinueWith(task => Display(task.Result)); 

タスクにCPSがあれば、1つを別のものに変換することができます。
 public static Cont<T> ToCont<T>(this Task<T> task) { return k => task.ContinueWith(task => k(task.Result)); } 


タスクの例


3つの非同期サービスがあるとします。 URLで老婦人のコンテンツを取得し、テキストを翻訳し、テキスト分析を行います。結果は数値になります。
 public static Task<string> GetPage(string url) { return Task<string>.Factory.StartNew(() => { Console.WriteLine("get page start " + url); Thread.Sleep(3000); Console.WriteLine("get page complete"); return "page content"; }); } public static Task<string> Translate(string text) { return Task<string>.Factory.StartNew(() => { Console.WriteLine("translate start"); Thread.Sleep(3000); Console.WriteLine("translate complete"); return "text translation"; }); } public static Task<int> Analyse(string text) { return Task<int>.Factory.StartNew(() => { Console.WriteLine("analyse start"); Thread.Sleep(3000); Console.WriteLine("analyse complete"); return 100; }); } 


CPSのこれら3つの機能の通過がどのようになるか想像してください。恐ろしくなり、すぐにリンクを使用して、タスクをモナドに変換します。
 var result = from x in GetPage("www.example.com").ToCont() from y in Translate(x).ToCont() from z in Analyse(y).ToCont() select z; result(Display); 

URLで翻訳されたページを取得するために別の場所が必要な場合は、別の方法で取り出すことができます
 private static Cont<string> GetPageTranslation(string url) { return from x in GetPage(url).ToCont() from y in Translate(x).ToCont() select y; } var result = from x in GetPageTranslation("http://habrahabr.ru") from a in Analyse(x).ToCont() select a; result(Display); 

ToContはどこでも使用されているので、余分なエンティティを削除してみてください。
 public static Task<T1> SelectMany<T, T1>(this Task<T> c, Func<T, Task<T1>> f) { return c.ContinueWith(task => f(task.Result)).Unwrap(); } public static Task<T2> SelectMany<T, T1, T2>(this Task<T> c, Func<T, Task<T1>> f, Func<T, T1, T2> s) { return c.ContinueWith(t => f(t.Result).ContinueWith(x => s(t.Result, x.Result))).Unwrap(); } 

タイプを展開するには展開が必要です
 Task<Task<T>> 
これは、2つのタスクを組み合わせて取得されます。
すべてのToContを削除すると、同じ結果が得られます。
 private static Task<string> GetTranslation(string url) { return from x in GetPage(url) from y in Translate(x) select y; } 

全体として、Taskは本質的に継続モナドであると仮定できます。
たとえば、たとえば、urlのリスト、Analyze値のリストを返す関数を作成します。 CPSでどのように見えるかを考えてください。
 private static Task<IEnumerable<int>> Analyse(IEnumerable<string> list) { var result = from url in list select from x in GetTranslation(url) from a in Analyse(x) select a; return Task.Factory.ContinueWhenAll(result.ToArray(), x => x.Select(y => y.Result)); } 

ContinueWhenAll-すべての結果を取得するために、すべてのタスクが完了すると待機します。

 var urls = new[] {"url1", "url2", "url3"}; var r = Analyse(urls); r.ContinueWith(x => Console.WriteLine(x.Result.Sum())); 


おわりに


このすべてが少なくとも実用的な意味を持っている可能性は低いため、このテキストは、異常なプログラミングのカテゴリーからの検討のための情報と見なすことができます。 また、誰もがIObservableおよびIObserverインターフェースとIObserver.OnNext(T値)インターフェースについて考えることができます。

PS良いことですが、awaitと同じ非同期に触れることは害になりませんが、cptをインストールすることは望んでいないので、それらについてかなり表面的なアイデアがありますが、ポイントは上記とあまり変わらない、つまり同じ作業が行われていると思いますタスクを使用しますが、リンクの代わりに、コンパイラを使用した計算フローの変換が使用されます。

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


All Articles