非同期プログラミングと計算式

C#5のasync / awaitに関する以前のメモ( パートIパートII )で、Haskell、F#、Nemerleなどの言語で同様のアプローチが実装されていることを書きましたが、C#とは異なり、これらの言語は高レベルの概念をサポートしています。これにより、言語レベルではなくライブラリとしてasync / awaitのスタイルで非同期計算を実装できます。 Nemerleでは、このコンセプト自体がライブラリとして実装されているのは面白いです。 この概念の名前はモナドです。 非同期計算に加えて、モナドを使用すると、リストの理解、継続、ダーティな関数をクリーンなブロックに変換し、状態を暗黙的にドラッグするなど、他の多くの利点を実装できます。

一部のモナドは、C#プログラマの「希望」をyieldコレクションまたはyield foreachおよびlambda式からyieldとして実装します。

この投稿の目的は、Nemerleを非同期プログラミングおよび計算式に導入することですが、F#を学習している人にとっても役立つ可能性があるため、Nemerleでの非同期プログラミングの実装は、F#に注目して行われました。 一方、他の言語の問題であるいくつかのタスク( すべての非同期呼び出しの後 )が、2、3行の計算式を使用してどのように解決されるかは、誰かにとって興味深いかもしれません。

おそらく、モナドとは何かを理解している人は誰でも、モナドとは何かについての記事を書いているでしょう。 私も例外ではありませんでした。 他の人がさらなるナレーションを理解できる一方で、知っている人が覚えているように、できるだけ簡潔に説明するようにします。

モナド


モナドは、言語にサポートが組み込まれている生成パターンです。 次のペアがこのパターンの中心にあります。

多相型

interface F<T> { ... } 

彼に対するいくつかの操作

 class M { static public F<T> Return<T>(T obj) { ... } static F<U> Bind<T,U>(F<T> obj, Func<T, F<U>> fun) { ... } } 

Returnを使用すると、モナド内の任意の値を「ラップ」し、バインドしてその上で変換を実行できます。 StringBuilderを使用して非常に弱い類似性を引き出すことができます。このコンストラクターには、コンストラクターパラメーターで初期値を渡し、Append *メソッドで変更します。

FをIEnumerableで置き換える場合、バインド署名はLinq SelectMany署名に似ています。 これは驚くべきことではありません。というのも、Linqも素晴らしい予約をしているので、モナドだからです。 ちなみに、PDC2010では、バートデスメットが「LINQ、Take Two-Realizing the LINQ to Everything Dream」というレポートで興味深い話をしました( リンク )。

Linqがモナドの場合、単純なLinq式を作成してみてください。

 var nums = Enumerable.Range(-2, 7); var sqrt = from n in nums where n >=0 select Math.Sqrt(n); 

M操作を使用します。 最初に、M操作を宣言します。

 static class MList { static public IEnumerable<T> Return<T>(T obj) { var list = new List<T>(); list.Add(obj); return list; } static public IEnumerable<U> Bind<T, U>(this IEnumerable<T> obj, Func<T, IEnumerable<U>> fun) { return obj.SelectMany(fun); } static public IEnumerable<T> Empty<T>() { return Enumerable.Empty<T>(); } } 

そして、Linq式を書き直します:

 var nums = Enumerable.Range(-2, 7); var sqrt = nums .Bind(n => n >= 0 ? MList.Return(n) : MList.Empty<int>()) .Bind(n => MList.Return(Math.Sqrt(n))); 

Linqの場合よりもさらに悪化しましたが、これはモナドサポートがC#に組み込まれていないためです。 Nemerleの場合、このコードは次のようになり、M操作を宣言します。

 class MList { public Return[T](obj : T) : IEnumerable[T] { def data = List(); data.Add(obj); data } public Bind[T, U](obj : IEnumerable[T], f : T->IEnumerable[U]) : IEnumerable[U] { obj.SelectMany(f) } public Empty[T]() : IEnumerable[T] { Enumerable.Empty() } } 

そして、Linq式を書き直します:

 def mlist = MList(); def nums = Enumerable.Range(-2, 7); def sqrt = comp mlist { defcomp n = nums; defcomp n = if (n >= 0) mlist.Return(n) else mlist.Empty(); return Math.Sqrt(n :> double); }; 

最初に、NemerleのdefはC#のvarの移植性のない類似物であり、ifは三項演算子(?:)であり、コンストラクターの呼び出しにはnewは必要ないことを思い出してください。 これまでのところ、comp演算子はモナド計算の開始をアナウンスし、次のパラメーターはM操作を提供し、計算自体が続行されます。

Linqと比較すると、1行ではなく3行ですが、これらの行は1つの変数で機能する通常のコードのように見え、実際には元のコレクションから新しいコレクションを生成します。 この例は教育目的で提供されています。以下は、通常のコードでは繰り返すのが非常に難しい例です。 仕組みを見てみましょう。

defcompは、モナド(この場合、IEnumerable [T]型)を(T型の)値に「変換」し、逆に値をモナドに変換するマジックオペレーターです。 実際、魔法はなく、ただの表現です

 defcomp n = nums; ... 

コンパイラによって拡張された

 mlist.Bind(nums, n => ...) 

計算式


Haskell言語について議論しているのであれば、モナドについての話はこれで終わります。 しかし、ハイブリッド言語(関数型/命令型)の場合、条件演算子、ループ、およびyieldなどの制御構造が存在するため、状況はもう少し複雑です。 これがどのように問題を引き起こすかを理解するために、M演算を通るループとループ内のdefcomp演算子を含むモナド計算を表現することができます。

この問題の解決法は非常に簡単です。たとえば、ブランチ演算子とループの変換を処理するM操作メソッドのセットに追加する必要がありますが、Wh​​ileには次のシグネチャがあります。

 public F<FakeVoid> While<T>(Func<bool> cond, Func<F<FakeVoid>> body) 

コンパイラが本体にモナド演算子を含むループを検出すると、最初にループ本体をバインドチェーンに変換します。BindはF <T>を返すため、このチェーンはラムダ "()=> body()"でラップできます。 Func <F <T >>の場合、コンパイラーはループ条件をラムダでラップし、これらのラムダをM操作のWhileに渡します。

各M操作はモナドを返す必要がありますが、ループは何も返しません。したがって、モナドにラップできる値はありません。 これらの目的のために、FakeVoidタイプのシングルトンが使用されます。

これで、計算式の非公式な説明ができます。これは、命令型言語のモナドです。 a-la haskellの場合、コンパイラーはdefcompを書き換えて、単項計算の内部で返すだけです。命令型言語の場合に既に述べたように、制御構造も書き換えられます。以下の表には、書き換えられるすべての演算子があります。
defcomp意味でモナドを拡張し、割り当てに近い意味
callcomp値が重要でないときに使用されるモナドを展開します
帰る引数をモナドにラップし、モナド計算のブロックの最後で使用されます。意味は関数から戻ることに近い
returncomp引数はモナドであり、モナド計算のブロックの結果としてこのモナドを返します。戻りとは異なり、再度ラップしません
利回り引数をモナドにラップし、return returnに似たアクションを実行します
yieldcomp引数-モナド、yieldが引数を再度ラップしないのとは異なり、yield returnと同様のアクションを実行します
if、when、unless、while、do、foreach、for、using、try ... catch ... finally通常の制御構造の単項バージョン

Mオペレーションのプロバイダーについてもう少し説明します。 公式には、それらはビルダーと呼ばれ、計算式を作成するときにアヒルの型付けが使用されます。つまり、ビルダーはコンパイラーが使用するメソッドを含む必要がありますが、ビルダーはインターフェイスを実装する必要はありません。 このソリューションを使用すると、計算式ですべての機能を使用する予定がない場合に、ビルダーを部分的に実装できます。 ところで、MListビルダーを作成するときにこのアプローチを既に使用しました(defcompとreturnのサポートのみが実装されています)。

インターフェイスが使用されないもう1つの理由は、M操作の署名により厳しい条件を課すことです。 タイプごとのモナドとM操作の互換性のみが必要です。 たとえば、上記の例では、モナドには1つのジェネリックパラメーターがあると想定されていましたが、いくつかのパラメーターを簡単に持つことができます。さらにナレーションを付けることは重要ではありませんが、これはContinuationモナドの例を使用して調べることができます。

独自のビルダーを作成する場合は、 計算式のソースを調べることをお勧めします

標準ビルダーの例



標準ビルダーは計算式ライブラリに組み込まれているため、インスタンスを作成してcompをパラメーターとして渡す代わりに、名前をパラメーターとして渡すだけで十分です。

一覧

リストビルダーは、言語の標準制御構成体、およびyieldおよびyieldcompをサポートします。 リスト[T](Nemerleの標準リスト)をモナドとして使用します。 このビルダーは、C#プログラマーの2つの長年の「ウィッシュリスト」を実装しているという点で興味深いものです。lambdafrom lambdaとyieldコレクションです。 最初に、記事の最初からのLinqクエリアナログを見てみましょう。

 def num = Enumerable.Range(-2, 7); def sqrt : list[double] = comp list { foreach(n in num) when(n >= 0) yield Math.Sqrt(n); } 

ご覧のとおり、リストビルダーを使用すると、関数を宣言することなくyield式を使用できます。また、オブジェクトへのリンクの代わりに使用することもできます。 このコードは、同等のlinq式よりも読みやすいと思われます。

次に、別の「ウィッシュリスト」を検討します。コレクションを生成します。最初にシーケンスを生成するローカル関数を宣言し、それを2回呼び出してコレクションを生成します。

 def upTo(n) { comp list { for(mutable i=0;i<n;i++) yield i } } def twice = comp list { repeat(2) yieldcomp upTo(3); } Console.WriteLine(twice); //[0, 1, 2, 0, 1, 2] 

私は自分のジェネレーターを書く必要があり、型のために「Enumerable.Range(0、3)」を使用しませんでした:yieldcompはモナドが入力されることを期待し、この場合の型はリスト[int]、および「Enumerable.Range(0、3) 「IEnumerable [int]を返します。 この矛盾を克服するために、列挙可能な別のビルダーがあります。

列挙可能

このビルダーは、Listビルダーをほぼ繰り返し、モナドのタイプとしてIEnumerable [T]のみを使用し、無限シーケンスを構築できます。 最後の例を書き直します。

 def twice = comp enumerable { repeat(2) yieldcomp Enumerable.Range(0, 3); } foreach(item in twice) Console.Write($"$item "); //0, 1, 2, 0, 1, 2 

配列

listおよびenumerableと同様に、配列は動作し、モナドのタイプとして配列[T]のみを使用します。

非同期

最も複雑ですが非常に便利なビルダーは、多くの点でC#の将来の非同期/待機に似ています。 既存の非同期コンピューティングを組み合わせて、非同期コンピューティングを構築するために使用されます。

yieldとyieldcompを除くすべての操作をサポートします。

このビルダーのモナド型はAsync [T]です。 このタイプのオブジェクトは非同期計算を記述し、その結果はタイプTの値になります(C#のTask <T>など)。非同期操作が値を返さない場合、Tの代わりに特定のFakeVoidタイプが使用されます。 バインド操作、そのタイプAsync [T] *(T-> Async [U])-> Async [U]は、関数による(Async [T]タイプの)非同期計算を「継続」し、この関数はタイプTのオブジェクトを入力(結果非同期計算)、Async [U]タイプの新しい非同期計算を返します。

もう1つのキータイプは抽象クラスExecutionContextです。その子孫のインスタンスは、非同期操作(たとえば、現在のスレッド、ThreadPoolのスレッド、またはSynchronizationContextの使用)の開始を担当します。署名は次のとおりです。

 public abstract class ExecutionContext { public abstract Execute(computatuion : void -> void) : void; } 

非同期操作を開始するには、非同期操作を記述するオブジェクトのStartメソッド(クラスAsync [T])を呼び出して、ExecutionContext型のオブジェクトを渡す必要があります。メソッドが引数なしで呼び出された場合、非同期操作はThreadPool.QueueUserWorkItemを使用して開始されます。

C#で非同期/待機実装を使用できる拡張機能(非同期CTP)には、既存のクラスを非同期操作で補完する多くの拡張メソッドが既にあります。 非同期は、モナド実装を使用してライブラリにそのような拡張機能を提供しませんが、既存のプリミティブに基づいてそれらを構築する簡単な方法を提供します。 たとえば、フレームワークの最初のバージョンから存在するリクエストを非同期的に実行する既存のHttpWebRequest署名の一部を考えてみましょう。

 public class HttpWebRequest : WebRequest, ISerializable { public override IAsyncResult BeginGetResponse(AsyncCallback callback, object state); public override WebResponse EndGetResponse(IAsyncResult asyncResult); } 

次に、これらのプリミティブを使用したモナドコンピューティングでの使用に適した非同期拡張を作成します。

 public module AsyncExtentions { public GetResponseAsync(this request : HttpWebRequest) : Async[WebResponse] { Async.FromBeginEnd(request.BeginGetResponse(_, null), request.EndGetResponse(_)) } } 

Nemerleの_は特殊文字であり、この場合はカリー化が使用されることを思い出してください(表記f(_)はx => f(x)と同等です)。 同様に、標準の非同期計算用のラッパーを作成できます。

Nemerleの(C#101)非同期サンプルから何かを書きましょう。たとえば、複数のWebページを並行して読み込み、タイトルを印刷します。GetHtml()およびGetTitle()拡張コードを省略しました。記事は既にドラッグされています。

 public PrintTitles() : Async[FakeVoid] { comp async { def response1 = HttpWebRequest.Create("http://www.ya.ru").GetResponseAsync(); def response2 = HttpWebRequest.Create("http://www.habr.ru").GetResponseAsync(); defcomp response1 = response1; defcomp response2 = response2; Console.WriteLine(response1.GetHtml().GetTitle()); Console.WriteLine(response2.GetHtml().GetTitle()); } } 

最初の2行では、非同期ページ読み込み操作が開始され、これらのメソッドは実行時に非同期操作を記述するオブジェクトを返します。コンパイラの観点からは、これらのオブジェクトのタイプはAsync [WebResponce](monad)です。 次の2行では、意味のモナドが拡張されます。意味の別のレベルでは、結果の期待を意味します。 最後の行では、結果が処理されます。

おもしろいことに、JavaScriptで行う正しい方法( すべての非同期呼び出しの結果を待つ)についての議論が非常に暑かったことがわかりました。お気に入り90、コメント100です。 しかし、例に戻りましょう。

覚えておくべき主なことは、モナドは生成パターンであり、非同期計算を記述する関数を作成したが、それを開始せず、PrintTitles()。Start()。GetResult()のように実行できることです。 実際、これは非常に重要です。エラーの原因になる可能性があるため、メソッドがAsync [T]を返す場合、このコードが計算を開始するのか、それを構築するだけなのかを認識する必要があります 。 区別するために、おそらく命名規則を使用する価値があります。たとえば、コンピューティングを開始するメソッドには非同期サフィックスが必要です。

C#のasync / awaitに関する2番目の記事で、awaitは非同期計算を開始したスレッドのSynchronizationContextで非同期計算の結果の処理を開始することを書きました。 Nemerleはこの点に関して非常に柔軟性があり、スレッド間で計算を転送することができます。 ボタンクリックハンドラーを考えます。

 private button1_Click (sender : object, e : System.EventArgs) : void { def formContext = SystemExecutionContexts.FromCurrentSynchronizationContext(); def task = comp async { Thread.Sleep(5000); callcomp Async.SwitchTo(formContext); label1.Text = "success"; } _ = task.Start(SystemExecutionContexts.ThreadPool()); } 

まず、現在のSynchronizationContextで計算を開始するExecutionContextを取得し、次に非同期操作を記述します:Thread.Sleepは重い計算をエミュレートし、実行コンテキストをスレッドの実行コンテキストguiに切り替えて結果を表示します。 計算自体は、ExecutionContextsスレッドプールで起動されます。

それは魔法のように見えますが、実際にはすべてがすでに起こっており、その意味が重要でない場合、callcompは単にモナドを明らかにします。 しかし、なぜそれを開示するのでしょうか? それは副作用と状態の問題であり、モナドの操作中に状態がそれらを介してドラッグされ、オープン時のモナドはこの状態にアクセスし、それを変更することができます。 この例では、状態はコードを実行するコンテキストの情報を保存し、この情報が変更されると、新しいコンテキストに切り替わります。 詳細については、 ソースを読むことをお勧めします。興味深いです。

Async.SwitchToに加えて、実行のフローに影響を与える他の興味深いモナドがあります。たとえば、Async.Yieldは、実行コンテキストは変更されませんが、実行コンテキストが変更されたことを示します。 場合によっては、これは何も行いません。ThreadPoolが使用された場合、このアクションはプールから別のスレッドへのジャンプを引き起こします。

おわりに


結論として、私はモナドが非常に豊富なトピックであることにのみ注意することができます。 この記事では、State、Cont(続き)、Maybe(別のC#ファンボーイウィッシュリスト)などの古典的なモナドには触れませんでした。 それらについては他の記事で読むことができます。私は実用的な説明をしようとしました。おかげでNemerleで非同期プログラミングとリスト/列挙可能なモナドを使い始め、内部で何が起こっているのかを知ることができます。

多くの点で、将来のC#でのawait / asyncの実装とNemerleでの非同期プログラミングへのモナドのアプローチは似ていますが、await / asyncをサポートするための注意点が1つあります。次のバージョンの言語が必要です。言語ではなく言語)。

私は質問にコメントして答えてうれしいです。

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


All Articles