いきなりですが、n月刊ラムダノートが創刊されました。めでたい。
で、この中にある @mametter さん執筆の記事『「コルーチン」とは何だったのか?』に僕はがっつり関わっていました。
後述しますが、僕と彼とのオンラインでのやり取りの中で記事が書き始められ、方針が決まっていった感じです。
なので、出版を記念してそのへんの経緯を振り返り、また僕なりの感想をまとめようかと思います。
……と思ったんですが、改めてやりとりのログを見返してたんですがアホほど膨大な量になっていて、とても全部振り返るのは無理だと悟りました。
なのでざっくりと。
当該の記事と一緒に読まれることを想定していますので、その記事で語られていることの説明はある程度省いています。
数時間前に @mametter さんが草稿を公開↓したので、そちらを読んでくれても構いません(できれば買ってほしいけど)。
目次:
経緯
きっかけ
去る2018年11月17日、仕事でPythonコードを書いていた僕が、ついasyncioパッケージ周りでコードを上手く書けないモヤモヤを夜になんとなく愚痴りました。
その中に、async/await で記述されたものを「コルーチン」*1 と呼んでいるらしい、なんかおかしい、という話をちょろっと入れたのです。
翌日の11月18日、僕の愚痴からアヤシイ匂いを敏感に感じ取った @mametter さんが思いをTwitterでぶっぱして、無事炎j……バズります。
外がいい感じで燃え盛る中、僕や @mametter さんがPythonコルーチンに感じ取った違和感はいったい何なのかを探る長い旅がスタートします。
コルーチンとは何かを探る旅
外がいい感じに炎……盛り上がっているさなか、最初に僕らの間で始まったのは、コルーチンが一番最初に活字にて著されたConwayの1968年の論文の読み会でした。
そこから23日までの5日間、とにかく色んな物を調べまくりました。思いつく限り列挙すると、
- Conwayの論文
- 非対称コルーチン(セミコルーチン)に関するDahlらの1972年の文献
- de Moura and Ierusalimschy, "Revisiting Coroutines", 2004
- コルーチンについて非常によくまとまっているサーベイ論文でオススメ。2000年代までのコルーチンがどういうものだったか、きちんと知りたければこの論文を読むのが良いと思います。
- ジェネレータを最初に提案した? CLU Icon なる言語の Technical Report
- PythonのPEP2つ(PEP342、PEP492)、その他Kotlin、JavaScript、C#、C++ のコルーチン関連のRFP
- 各言語でのコルーチンの用法を解説しているブログ(英語、日本語)や Slideshare/SpeakerDeck たくさん
その他もろもろ。
そいつらを読みながら、コルーチンの定義とその変遷の様子に関してああでもないこうでもないと2人で延々議論していました。
同時に、Pythonコルーチンを使って対称コルーチンを再現してみたりもしていました。(この結果は省略。そのうち紹介するかも。)
そんな感じで、自分たちがなんとなく「これがコルーチンだ」と思っていたものを、適宜事実と照らし合わせて修正、更新、あるいは補強していく作業でした。
70年代以前の論文を短い期間にたくさん読んだのも初めてで、自分たちがしていることをある時期から2人の間で「考古学」と呼ぶようになるのですが、実際、考古学というのはこういう学問なんだろうなと思った次第。
執筆が始まる
19日に入って、裏側で延々と議論していた内容を反映して、 @mametter さんが今回の記事の草稿に当たるものを書き始めます。 当初は自分のブログに書くためのもので、もっと簡素だったし、ツイートだと書ききれない @mametter さんのポジショントークというか意思表明というかそういうノリのもので、なので @mametter さんの主張が色濃く残っていました。
23日頃を境に一旦その執筆の手は止まり、2人の間で多少話題になったり、PPL*2でなにかの形で発表しようか? などという検討をしたりはしていたものの、特段の進捗はなくそのまましばらく記事は放置されます。
そして出版へ
年が明けて2019年の1月下旬、@mametter さんが書き溜めてあった草稿をラムダノート社長の @golden_lucky さんに見せたところ好評を得たようで、@golden_lucky さんが以前から構想していた雑誌に載せる方向で話が進んだようです(このへんは僕は直接会話に立ち入ってないのでわからない)。そこで記事の執筆も再開されます。
で、再開早々僕が主張したことは、記事の中で「何がコルーチンか/コルーチンでないか」という断定を避けた方がいい、ということでした。
上述の通り元々は @mametter さんのポジショントークだったので当然そのへんは @mametter さんなりの意見が含まれていたのですが、そこを結論として持ってくるより、むしろ僕らが調べた事実に基づいて「コルーチンの定義が揺らいでいる/変遷している」ということだけを述べ、それ以上の判断は読者に委ねたほうがいいと考えました。
結果として記事もそういう方向に軌道修正されたし、実際そうすることでいろんな立場の人がニュートラルに読める内容になり、記事の価値は上がったのではないかと個人的には思っています。
所感
ここからは、完全に僕個人の雑感です。
今回色々見てまず感じたことは、最近の言語は並列・並行にまつわる概念がごっちゃになっているということですかね。 ファイバー、コルーチン、軽量スレッド、future / promise あたりがごちゃ混ぜになってできているように感じます。 実際のところ、このへんを全部区別できる人がどれだけいるんでしょう?
1980年時のコルーチンの定義 と Pythonコルーチン
ということで、記事では最終的に「これがコルーチンだ」という断定は避ける形になりました。 ただ、 記事には載せられていないのですが、実は Marlinが「コルーチンが満たすべき基礎的性質(fundamental characteristics)」というものを書いています (Marlin’80)。 それがこれ。
the values of data local to a coroutine persist between successive occasions on which enter it (that is, between successive calls)
(コルーチン内にローカルに存在する値は連続する “call” の間で保持される)the execution of a coroutine is suspended as control leaves it, only to carry on where it left off when control re-enters the coroutine at some later stage.
(コルーチンの実行は制御が離れたら停止し、その後のどこかでコルーチンに再入したときに制御の離れた箇所から再開する)
カッコ内は僕が今適当に訳しました。明らかな誤訳でなければ微妙なところは見逃して。
これが記事に使われなかったのは、この定義もまた微妙な問題を孕んでいたからです。 どこらへんの解釈かというと “call” です。純粋に考えればコルーチン間の呼び出し/被呼び出しのことでしょう。 つまり、コルーチン間の非同期な呼び出しがあることが前提なんです。
ところが、ここにPythonコルーチンを持ってくると、解釈がとてもややこしくなります。
Pythonコルーチンって、他のコルーチンへの呼び出しで非同期的な中断するわけじゃないんですよね。
呼び出しで非同期的な中断をするのは、呼び出し先のコルーチンか、そいつが呼び出すさらに先のコルーチンか、その先か……がsleepするときだけなんですよ。
呼び出し=中断の合図ではないんです。
でも、Marlinがこれを著したときにあったのは、対称コルーチンと非対称コルーチンだけでした。 この2つは他のコルーチンを呼び出すことで非同期的な中断をする仕組みで、呼び出し=中断 なんです。 だからPythonコルーチンはこのMarlinの定義がそのまま当てはまるわけじゃないです。
てか、Pythonコルーチンってぶっちゃけコルーチンというより、仕組みとしてはスレッドに近いんですよね。
sleepで寝て、寝てる間は一緒に動いてる他のスレッドのどれかに制御が移る。sleepが解ければスケジューラによってそのうち制御をもらって処理を再開する。
これ、スレッドなんですよ。
これにfutureの仕組みを足したものがPythonコルーチンの正体だな、と。
ではPythonコルーチンはコルーチンではないのか
ただ、ある想定を置くことで、旧来のコルーチンを使ったモデルと考えられなくもないんです。
それは、スケジューラ(Pythonコルーチンでいうイベントループ)を特別なコルーチンと考えることです。
なのでPythonコルーチンのコンテクストスイッチ(敢えてスレッド用語を使う)は、イベントループがコルーチンを呼び出すことでそのコルーチンが処理を再開する。
sleepすることでイベントループに処理を戻す。
イベントループがresumeして、コルーチンがsleepのタイミングでyieldする非対称コルーチンってことです。
これは、Dahl et al. が大昔に書いている手法で、また、Crystalのファイバーがこういうモデルを採用してたはず。
(僕が他の言語も調べた中で、Pythonコルーチンに一番似てるなと思ったのはCrystalのファイバーでした。)
このモデルを前提に、イベントループ/スケジューラによるコンテクストスイッチを先のMarlinの定義で言う”call”とみなしていいか、という点で 僕と @mametter さんで解釈が分かれて、最終的にこの定義は使えないという話になったのです。
結局何がいいたいのか
過去の論文や他の言語の仕様も調べまくって色々考えましたが、結局Pythonコルーチンって、個人的にはあまりコルーチンとは呼びたくないんですね、やっぱり。軽量スレッドと呼びたい何かです。
それが悪いというわけではないんですが、ただコルーチンとしてみるとかなり貧弱な機能しかないので、不便です。
中断する際に他のコルーチンを呼び出して、そいつに値を渡すとかできないですからね。
さっきCrystalのファイバーが似てると書きましたが、Crystalは他のファイバーとの非同期な通信機構があります。
Pythonコルーチンにはない。コルーチン間の非同期通信はfutureでやるんです。
Pythonコルーチンは一般的なコルーチンと思うとツラくて、むしろfutureを使った非同期モデルを指向してコードを書くべきなんでしょう。 積極的にfutureオブジェクトを使いまわしていくのがいいのかな、というのが今ん所のPythonコルーチンに対する僕の所感です。