プログラムでは状況に応じてプログラムの状態が変わる状態遷移というものを考慮する事が多いと思います。
非同期処理での状態遷移でありがちなパターン
通常のプログラムでは上から順番に手続き的にプログラムを書けば状態遷移を書けますが、アニメーション、ゲーム、通信周りのプログラムではハードウェアやユーザ入力の完了を待つ間に他の処理を行わなければならない事が多いためそのようには書けない事が多いです。
こんな雰囲気のコードを書いた経験があるのではないでしょうか
// ユーザの入力やフレームの更新の情報が時折送られてくる
var message = pullMessage()
// 状態毎にmessageの送り先を振り分ける
switch(mode) {
case "ANIMATION_START":
start(message)
mode = "ANIMATION_FRAME1" // 次の入力ではFRAME1に遷移
break;
case "ANIMATION_FRAME1":
if(frame1(message)){
mode = "ANIMATION_END"
} else {
mode = "ANIMATION_START"
}
break;
case "ANIMATION_END":
end(message)
mode = "ANIMATION_START"
break;
}
上記プログラムは下記の面で扱いづらさがありますね。
- 遷移が分かりづらい(コードの中に埋もれて見からなくなることも)
- 遷移の条件を書きづらい
- 状態が追加された時の改修が重い
- 状態間で変数の共有を始めるとよりスパゲッティ化が加速し易い
同期的なプログラミングでは超簡単に書けた状態遷移がここまで複雑になるのには納得いきませんね。
そこでジェネレータもといコルーチン
PythonやJavaScriptにはジェネレータと呼ばれる機能があります。下記のような機能です。
def generator():
yield 2
return
function* generator() {
yield 1;
return
}
PythonもJavaScriptもほぼ同様な使用感です。
コルーチンとは
ジェネレータという機能は、古来非対称コルーチンと呼ばれる物だったらしいです。
コルーチンはどういうものかというと下記のような動きをする機能の事です。
2つのコルーチンは別々のプログラムですがメッセージをやり取りしあいながら進んでいきます。
ジェネレータで登場するyieldがメッセージのやり取りを担当していることが分かるでしょうか?
ジェネレータでコルーチンを書く
さてジェネレータをコルーチンのように使うにはどうしたら良いでしょう。
JavaScriptで例を書きます。
// コルーチンb
function* coroutine_b() {
const from_a1 = yield // aからのメッセージを受け取り
yield "to_b" // aにメッセージを返却
const from_a2 = yield // bからのメッセージを待機
}
// コルーチンa
function* coroutine_a() {
val co_b = generator()
co_b.next(message) // bにメッセージを送信
co_b.next() // bからメッセージを受信
co_b.next(message) // bにメッセージを送信
}
yieldはメッセージを返却する他に受け取る事もできます。
アニメーションの状態遷移をコルーチンで書く
記事上部のアニメーションの状態遷移をコルーチンで記述してみましょう。
JavaScriptで書きます。
function* generator() {
while(True) {
const start_message = yield
start(start_message)
const frame1_message = yield
if(!frame1(frame1_message)) {
continue
}
const frame1_message = yield
const from_a2 yield
}
}
val co = generator()
val message = pullMessage();
co.next(message);
手続き的なプログラムとほぼ同じような感じで状態遷移が書けているのがわかるでしょうか
if文やwhile文がそのまま状態遷移を表すことができていて、バリデーションが簡易化し、状態遷移も追いやすくなったと思います!
状態間で変数をやり取りする際もスコープの概念を使用でき、必要ない箇所では使用できないようになり手軽にやりとりできます。
コルーチンプログラム分割の方法
簡易なプログラムでは上記のテクニックだけで済むと思いますが、ゲームなどの状態遷移が細かいアプリケーションではコルーチンの分割や状態の中でサブの状態管理を行いたくなると思います。
JavaScriptの場合yield*
、Pythonではyield from
を使えばメソッドから受けとったコルーチンをそのまま自分のコルーチンのように相手流してくれるのでこれを使用します。
JavaScriptでの例を記述します。
function* mode1() {
input = yield ;
yield mode1_logic(input);
}
function* mode2() {
input = yield ;
yield mode2_logic(input);
}
// コルーチン達を集約する箇所
function* comain() {
yield* mode1();
yield* mode2() ;
}
手続き的なプログラミングでreturnを連鎖させた時とほぼ同じ使用感ですね。
async awaitとコルーチンの併用
今どきのJavaScriptではasync/awaitを使用した非同期処理を多用すると思いますが、
コルーチンの中で使用するには下記のように書きます。
async function* generator() {
val a = await fetch_data(); // 非同期でデータを取得する処理
yield a;
}
まとめ
どうでしょうか、ジェネレータを本来のコルーチンとして使用する事で、かなり簡易に記述できる物があるのではないでしょうか。
私は特にWeb WorkerやWebSocket、HTTPサーバなど通信系のプログラムを記述する時に使います。
参考文献
2 「コルーチン」とは何だったのか?
https://www.lambdanote.com/collections/n/products/nmonthly-vol-1-no-1-2019-ebook