C#の非同期プログラミングモデルは、一般的なモナドパターンのインスタンスであるF#の非同期ワークフローと非常によく似ています。実際、C#イテレータ構文もこのパターンのインスタンスですが、追加の構造が必要なため、単純なモナドではありません。
これを説明することは、単一のSO回答の範囲をはるかに超えていますが、重要なアイデアについて説明します。
モナディック操作。
C#非同期は、基本的に2つの基本的な操作で構成されます。await
非同期計算と非同期計算の結果を行うことができます(return
最初のケースでは、これは新しいキーワードを使用して実行されますが、2番目のケースでは、すでに言語にあるキーワードを再利用しています)。
一般的なパターン(モナド)に従っている場合は、非同期コードを次の2つの操作の呼び出しに変換します。
Task<R> Bind<T, R>(Task<T> computation, Func<T, Task<R>> continuation);
Task<T> Return<T>(T value);
これらは両方とも、標準のタスクAPIを使用して非常に簡単に実装できます。最初のAPIは基本的にとの組み合わせでContinueWith
ありUnwrap
、2番目のAPIは値をすぐに返すタスクを作成するだけです。上記の2つの操作を使用します。これは、これらの操作の方がアイデアをよりよく捉えているためです。
翻訳。重要なことは、非同期コードを上記の操作を使用する通常のコードに変換することです。
e
式を待ってから結果を変数に割り当て、x
式(またはステートメントブロック)を評価する場合を見てみましょうbody
(C#では、式の内部で待つことができますが、最初に結果を割り当てるコードにいつでも変換できます変数):
[| var x = await e; body |]
= Bind(e, x => [| body |])
私はプログラミング言語で非常に一般的な表記法を使用しています。の意味は、式(「意味括弧」内)を他の式[| e |] = (...)
に変換することです。e
(...)
上記の場合、。を使用した式があるとawait e
、それは操作に変換されBind
、本体(awaitに続く残りのコード)は、2番目のパラメーターとしてに渡されるラムダ関数にプッシュされますBind
。
ここで面白いことが起こります!残りのコードをすぐに評価する(または待機中にスレッドをブロックする)代わりに、Bind
操作は非同期操作(e
タイプはTask<T>
)を実行でき、操作が完了すると、最終的にラムダ関数を呼び出すことができます(継続)体の残りの部分を実行します。
変換の考え方は、あるタイプを返す通常のコードをR
、値を非同期的に返すタスクに変換することです。つまり、ですTask<R>
。上記の式では、の戻り型Bind
は実際にタスクです。これが私たちが翻訳する必要がある理由でもありますreturn
:
[| return e |]
= Return(e)
これは非常に簡単です。結果の値があり、それを返したい場合は、すぐに完了するタスクでラップするだけです。これは役に立たないように聞こえるかもしれませんが、操作(および翻訳全体)でaを返す必要があるTask
ため、を返す必要があることを忘れないでください。Bind
より大きな例。await
複数のを含むより大きな例を見ると、次のようになります。
var x = await AsyncOperation();
return await x.AnotherAsyncOperation();
コードは次のように変換されます。
Bind(AsyncOperation(), x =>
Bind(x.AnotherAsyncOperation(), temp =>
Return(temp));
重要なトリックはBind
、コードの残りの部分を継続に変換することです(つまり、非同期操作が完了したときに評価できるということです)。
継続モナド。C#では、非同期メカニズムは実際には上記の変換を使用して実装されていません。その理由は、非同期のみに焦点を当てると、より効率的なコンパイル(C#が行うこと)を実行して、ステートマシンを直接生成できるためです。ただし、上記はF#で非同期ワークフローがどのように機能するかとほぼ同じです。Bind
これは、F#の追加の柔軟性の源でもあります-独自に定義できReturn
、シーケンスの操作、ロギングの追跡、再開可能な計算の作成、さらには非同期計算とシーケンスの組み合わせなど、他のことを意味することができます(非同期シーケンスは複数の結果をもたらす可能性があります) 、しかし待つこともできます)。
F#の実装は継続モナドに基づいています。つまり、F#のTask<T>
(実際にはAsync<T>
)はおおよそ次のように定義されます。
Async<T> = Action<Action<T>>
つまり、非同期計算は何らかのアクションです。引数として(継続)をAction<T>
指定すると、何らかの作業が開始され、最終的に終了すると、指定したこのアクションが呼び出されます。継続モナドを検索する場合は、C#とF#の両方でこれについてのより良い説明を見つけることができると確信しているので、ここで停止します...