Waterloo大学の助教授であるYizhou Zhang氏は、双方向の代数的エフェクト (bidirectional algebraic effects) を発表した。これは、双方向の制御フローをサポートしながら、現在の制御フローパターン (例外、promise、generator など) を包含する新しいプログラミング抽象化だ。新しい型付きの抽象化により、宣言されたすべてのエフェクトが処理され、誤ったエフェクト (たとえば、間違ったハンドラによって) が処理されないことが保証される。
プログラミング言語アプリケーションに関するカンファレンスであるSPLASHでの講演で、Zhang氏は最初に、ますます複雑になる制御フロー機能が多くの言語で採用されたことを思い起こした。JavaScriptに関して、ECMAScript 3で例外が導入された。ECMAScript 6 (ES6またはES2015とも呼ばれる) は、例外をスローする可能性のある promise と generator を追加した。ECMAScript 8 (ES2017) は、後に async が追加された。ECMAScript 9 (ES9) が続けて async で generator 関数を補完した。
それからZhang氏は説明した:
ソフトウェアはますますイベント駆動になっています [なりました]。コールバック関数はイベント駆動プログラミングの従来のパターンですが、制約のないコールバックは複雑になり、アプリケーションが成長するにつれて理解するのが難しくなります。したがって、現在、プログラミング言語では、generator や async–await などの高度な制御フロー転送機能のサポートを組み込むことが流行しています。これらの機能は、非同期のイベント駆動型コードのより構造化されたプログラミングをサポートします。
代数的エフェクト (algebraic effects) は、プログラマが独自の制御効果を定義できる強力な代替手段として登場しました。[…] 例外、generator、async–await など、さまざまな機能が含まれています。シンタックス (つまり、一連のエフェクト操作) とセマンティクス (つまり、これらの操作のハンドリング) の適切な分離 [… 提供します]。
ただし、これらの高度な言語機能が手元にある場合でも、今日のプログラマは、特定の複雑な制御フローパターンを管理するのが難しいと感じています。
特に、Zhang氏は、共通の制御言語機能は、すべてのエフェクトが実際に処理されることを保証しながら、双方向の制御転送をキャプチャするのに十分な表現力がないと主張した。Zhang氏は、主流の言語で一般的に見られる既存の制御フロー構造の問題を次のように要約している:
(出典: SPLASH talk)
プログラマは確かに JavaScript の promise と async/await を使用して非同期計算を処理するときにフットガンが発生する可能性があることに気づいた。Ian Segers氏は、ブログ投稿で、ジュニア開発者とのコードレビュー時に取り上げられたエラー処理の問題について詳しく説明している。try...catch
ブロックの promise は、catch
ブロックのためにリジェクトされた promise の処理の await
ができなければならない。
async function thisThrows() {
throw new Error("Thrown from thisThrows()");
}
try {
thisThrows();
} catch (e) {
console.error(e);
} finally {
console.log('We do cleanup here');
}
// output:
// We do cleanup here
// UnhandledPromiseRejectionWarning: Error: Thrown from thisThrows()
たとえば、前のコードではスローされた例外をキャッチしない。UnhandledPromiseRejectionWarning
警告は、実行時にのみ表示される。Jake Archibald氏は、await
対 return
対 return await
の微妙な点について説明した。CatchJS の背後にあるチームは、非同期で発生する例外の落とし穴を調査した。例外は別のコールスタックでスローされる。つまり、エラーがアプリケーションコードの外部に伝播する可能性がある。
function fails3() {
return new Promise((resolve, reject) => {
setTimeout(function() {
throw new Error();
}, 100);
});
}
async function myFunc3() {
try {
await fails3();
} catch (e) {
console.log("that failed", e); //<-- never gets called
}
}
したがって、JavaScript の onunhandledrejection
グローバルハンドラは、一部の例外では間違ったハンドラである可能性があり、Q と Bluebird の promise ライブラリと同様に onPossiblyUnhandledRejection
と呼ぶ方がよさそうだ。CatchJS チームは次のように述べている:
promise を処理するとき、エラーが将来のどの時点で処理されるかを知る方法はありません。promise は
reject()
を呼び出す可能性があり、一部のコードは10分後に到達し、その promise で.catch(() => {})
を呼び出す可能性があります。その場合エラーが処理されます。
Zhang氏はC#で同様の問題をレポートした:
static byte[] HttpGet(String url);
static async Task<Json> HttpGetJson(String url) {
Task<Json> t = Task.Run(() => HttpGet(url));
byte[] bytes = await t;
return JsonParse(bytes);
}
static async Task Main() {
Task<Json> t = HttpGetJson("xyz.org");
... // do things that do not depend on the query result
Json json = await t; // block execution until query terminates
...
}
C# コンパイラは、例外ハンドラが提供されていない場合でも前のプログラムを実行できる。そして、非同期クエリで例外が発生した場合にはプログラムはクラッシュする。Zhang氏は関連する論文に次のように書いている:
JavaScript の場合、状況はさらに悪化します: 非同期で発生した例外は、他の方法でキャッチされない限り、沈黙の中に飲み込まれます。このようなハンドルされない例外は、JavaScript プログラムの一般的な脆弱性として認識されています。
前述の問題 (未処理または誤って処理されたエフェクト) に、generator に関連する問題を追加する必要がある。generator は、generator とそのクライアント間の双方向の制御フローを可能にする。ただし、クライアントが反復されたデータを同時に変更することはできない。Zhang氏は、いくつかのユースケースについて詳しく説明した:
優先キューを反復処理するクライアントは、受信した要素の優先度を変更したい場合があります。同様に、データベースレコードのストリームを反復処理するクライアントは、それらのレコードの1つをデータベースから削除したい場合があります。
Zhang氏は、既存の制御フロー構造との既存のトレードオフについて説明した後、双方向の代数的エフェクト (bidirectional algebraic effects) と呼ばれる代数エフェクトを一般化する可能なソリューションについて説明した。双方向の代数的エフェクトは、双方向の制御フローを可能にしながら、未処理または誤って処理された効果がないことを静的に保証する。
代数的エフェクト (algebraic effects) は、これらのシグネチャの実装として、制御効果およびハンドラのシグネチャをサポートする。この場合、効果的なコードは、動的なコールスタックをハンドラに伝播する効果を引き起こす。generator の機能は次のように代数的エフェクトで複製できる (Node イテレータの例):
// Effect signature
effect Yield[X] {
def yield(X) : void
}
// Iterator
class Node[X] {
var head : X
var tail : Node[X]
...
def iter() : void raises Yield[X] {
yield(head)
if (tail != null)
tail.iter()
}
}
そして、Node
イテレータは次のように使用できる:
try { node.iter() }
with yield(x) {
print(x)
resume()
}
try... with
ブロックを使用すると、エフェクトを引き起こす可能性のあるコードを、それらを処理するコードから分離できる。エフェクトおよびイテレータ宣言レベルでの型シグネチャは、構造に型の安全性を提供するのに役立つ。
双方向エフェクトは、Zhang氏が効果的な効果と呼んだものを上げることによって、イテレーターのクライアントがイテレーターの状態を変更できるようにすることで、さらに進んでいる。効果的な効果を使用すると、エフェクトハンドラは、開始エフェクトが発生したサイトとは反対の方向に伝播する後続のエフェクトを発生させ、プログラムフラグメント間で情報と制御を転送できる。
前の図は、例外をスローできる非同期ジェネレーターを実装するために使用される Async
効果、Exn
(例外) 効果、および Yield
効果を示している。Yield
の効果的な効果は、Async
効果または例外を発生させる関数によって処理される場合がある。グラフは JavaScript で発生する可能性があるように、型安全な方法で、未処理の例外なしにエフェクトを使用して実装できる3者間制御フローを示している。
Zhang氏は説明した:
非同期計算をスケジュールし、promise を生成するイテレーターを使用できます。これは、待機 (await) すると、イテレーターによって処理される例外を発生させる可能性があります。基礎となる制御フローは複雑ですが、効果の静的チェックが効果処理を適用する場所に関するガイダンスを提供するため、依然として管理可能です。また、効果的な効果のシグニチャを表示して、2つのプロセス間のコミュニケーションを振り分けることもできます。
関連する論文で、Zhang氏は、双方向の代数的エフェクトにより、宣言されたすべての効果が処理され、誤って (たとえば、間違ったハンドラによって) 処理されないことが保証されることを証明した。
Yizhou Zhang氏の全講演は、より詳細、例、およびイラストを含めてオンラインで入手できる。SPLASH は、ソフトウェアの構築とデリバリーすべての側面を網羅するプログラミング言語のアプリケーションに関するカンファレンスだ。