fs2.Pull#compile 勉強会 その4
今回やりたいこと
cats-effect の IOFiber の実装 (つまり、cats.effect.IO から Future などへどう変換するか) 、の中身を見る
ラムダ計算 ✅ → (単純なスタックベースの抽象機械 →) CEK 機械 ✅ → IOFiber
CEK 機械は…
call-by-value (「値呼び」と訳されることがある)、つまり、「関数の引数を評価しきってからβ簡約する」ような実行方式
引数よりも先に関数側を評価する
組み込みオペレータ呼び出しに関しては、「左から右」に引数を評価していく
この機械をどう実装したかというと、
機械の状態として、次の二つのモノの組を持つ
これから評価しなければならない (「まな板の上に置かれた」) クロージャ
クロージャは <項t, tの自由変数からクロージャへの割り当て> という 組
そのクロージャを仮に評価し切れたら、そのとき何をしなければならないか
このデータは「継続 (Continuation)」である。継続渡しスタイル などで出てくる継続と同じ 一般に、「ある時点から見たプログラムの残りの部分 (を表すデータ構造のこと)」を継続と呼ぶ (Wikipedia での定義による) 「まな板の上に置かれた」クロージャの評価を進められそうならそうしていき、その際に未評価の部分を一旦脇に置いておく必要があるときには、「戻ってきたときにその未評価の部分についてどうこうする」という情報を継続側に積む
「まな板の上に置かれた」クロージャが値になっていたら (それ以上評価できなくなっていたら)、継続の一番最初の部分を切り崩し、値を用いて「次に評価しなければならないクロージャ」を作る
これを継続が空 (HALT) になるまで続ける
IOFiber は、このような問題 (= プログラムを如何にして実行すればよいか) の切り分け方を応用したものだと思える
今回の旅のしおり
とりあえず、メモリバリアについては無視します
次回以降余裕があったら話す とりあえずすべての変数を volatile だと思っておこう
volatile のことを聞いたことが無ければとりあえず青い薬を飲んでメモリキャッシュの話は忘れましょう 聞いたことがある人はニヤニヤしといてください windymelt.icon ニヤニヤ
トレーシング (tracingEvents) も無視します
索引
例えば、IOFiber のメンバーはこんな感じ
code:Scala
// Listtype V; key: IOLocalV; actual: V } から生成したような Map[IOLocal?, ?] // 「各 K/V pair の間で型が合ってる」ような Map (Map を使う限り型システムに載らない情報なので、こういう型になってる)
// windy: A =>> Map[IOLocalA, A] // ↑ではなかった。↑の例だと全部のペアでAに大統一されてしまう
privatethis var localState: IOLocalState = initState // 今 Fiber が実行に使っている ExecutionContext (最初は startEC)
privatethis var currentCtx: ExecutionContext = startEC /*
* Ideally these would be on the stack, but they can't because we sometimes need to
* relocate our runloop to another fiber.
*/
// conts と objectState は結託して継続を保存している
privatethis var conts: ByteStack.T = _ // objectState は継続の引数を保存している
privatethis val objectState: ArrayStackAnyRef = ArrayStack() // OnCancel 時に finalizer を積む場所
// 「今、もしこの瞬間にキャンセルされたら、どの finalizer を (どの順番で) 実行しないといけなかったのか」を保存している
// 実際にキャンセルされた時には、この Stack の上から順に finalizer を実行していく
privatethis val finalizers: ArrayStack[IOUnit] = ArrayStack() // Fiber が終了したときに呼ぶべきコールバックを覚えておく場所
// _join (この Fiber を外側から見ている観測者にとっては、「終わったら通知してくれ」というリクエスト) を完了させるために使う
// windy: フードコートで渡される鳴るやつを思い出した
privatethis val callbacks: CallbackStack[OutcomeIOA] = CallbackStack.of(cb) // Fiber が前回の run (「飽きるまで走ったとき」)の最後に何をしていたかを記録する 1 Byte
privatethis var resumeTag: Byte = ExecR // Fiber が前回の run の最後に何を _cur0 の余剰として遺したのかを記録する
privatethis var resumeIO: IOAny = startIO // Work-stealing thread pool と blocking (unbounded) thread pool の組とか、どれぐらいの頻度で「飽きる」のかとかの、
// Fiber のいわゆる設定情報
privatethis val runtime: IORuntime = rt // Tracing 機能を実現するための、trace を入れたリングバッファ (デフォルトで size = 16; IORuntimeConfig.DefaultTraceBufferSize)
// なぜ stack じゃなくて ringbuffer なのか、というと、例えば tailRecM とかで「意味的には」コールスタックをどんどん深めていくような場合でも
// メモリを無限に食われると困るので、過去 (tailRecM をするよりも前) のトレースを犠牲にしてでもメモリ使用量を抑えるため
privatethis val tracingEvents: RingBuffer = if (TracingConstants.isStackTracing) RingBuffer.empty(runtime.traceBufferLogSize) else null
// 自分はキャンセルされたか?
privatethis var canceled: Boolean = false // Uncancelable がいくつ重なっているか?
privatethis var masks: Int = 0 // 終了処理の真っ只中か?
privatethis var finalizing: Boolean = false // この Fiber の最終結果で、最初は null
@volatile
privatethis var outcome: OutcomeIOA = _