fs2.Pull#compile 勉強会 その0
Cosense の練習セクション
code:scala
def foo = println(42)
$ f
aaa
aa
a
inline-code
勉強会の目的・目標
目安の難易度は ⭐⭐⭐⭐✰ です
Kory.icon「目標は大きく」
fs2 には Stream[F[_], A] というたいへん便利なものがある これは、定義 を見てみると、 Stream[F[_], A] = Pull[F, A, Unit] として定義されている Stream[F, A] を「実行する」、つまり、F[Vector[A]] に集める (Rust で言う .collect_into_vec) といった操作をする時に、.compile という操作をすることになっているが、これも中身を見に行くと Pull[F, A, Unit] の .compile として書かれている (間に Compiler[F, G] と呼ばれるものが挟まっていて、これがどう Pull[F, A, Unit] を G 製のプログラムに変換するかを知っている)。
「つまり、Pull の .compile を理解すれば、Stream[F, A] を実際に使うときに何が起こっているのか (そしてつまり、Stream が何を表現しうるものなのか) を完全に理解™することができる」Kory.icon これを、アイデアのレベルから実装のレベルまで理解したい!というのが、この勉強会のモチベーションであり、目標
参加者
Kory.icon
Scala 9割 と Rust 1割 くらいで生きている
functional effect system 大好き人間
windymelt.icon
http4sにStream渡すくらいならできます
たまに.compileむずかしいよ〜と思っている
雰囲気で書いています
chen.icon
最近は Scala しか書いてない
fs2 は typelevel/fs2 に PR もしたけど業務でも趣味でも雰囲気で書いてる
hsjoihs.icon
Scala 書いたことなし
中学の頃から Haskell と戯れ、大学 1 年で Rust と知り合い、暮らしてきた
言語処理系とはどういうものであるか、若干分かっている
そういや非同期を全然やってきてないな
参加者の前提知識
Kory.iconが設問を並べていくので、参加者がそれらに回答していく、という感じで、今持ち合わせている道具を把握していきます
どの辺の道具をどれくらい使っているか
cats.effect.IO 、Monix、ZIO、Kyo などの関数型エフェクトシステムを (Scala、他言語問わず) 使ったことがある?
Kory.icon: IO / Monix をよく使っている ここ3年くらいは IO 無しのコードより IO アリのコードの方が多く書いている
chen.icon: 趣味でも仕事でも IO 無しで Scala を書く経験をほぼしてないくらいには IO ネイティブ
windymelt.icon: 趣味でIOはよく触っているが仕事では全然やっていない
hsjoihs.icon: 7 年前ぐらいは Haskell の IO にモナドトランスフォーマーをたくさん被せてました。エフェクトってやつを使うとこれがマシになると聞いてる
IO などのエフェクトシステムで「並行性」 (cats 周辺で言う .parTraverse による並行実行や、Queue によるチャンネルベースでの協調並行処理、などなど) を扱ったことがある?
Kory.icon:
chen.icon: 最近はたまーに使う
windymelt.icon: たまにやっている。Balanceがfs2から消えていたので自作したりした
hsjoihs.icon: 並行性に触れ合ってこない人生だった
IO などのエフェクトシステムで「リソース安全性」 (cats 周辺で言う .bracket や Resource) を陽に扱ったことがある?
Kory.icon:
chen.icon: 結構使う
windymelt.icon: 陽にはあまりないかも(Resource作ることはある)
hsjoihs.icon Haskell に handle や bracket ってものがあったな~ という記憶
IO などのエフェクトシステムで「キャンセル / キャンセル安全性」(cats 周辺で言う .uncancelable / .cancel) を陽に扱ったことがある?
Kory.icon:
chen.icon: 存在や表面的にどういうことが起きるかは知ってるけど全く使ってない
windymelt.icon: 長時間ジョブのキャンセルとかエラーハンドリング系ではたまにある
hsjoihs.icon: カラオケボックスでこりーさんからたくさん話を聞いたけど、実感しきれてはいない
IO などのエフェクトを Future であったり、Pekko Stream であったりのより低級な表現に変換して、それを (cats-effect などとは別の) 実行環境 (Future なら ExecutionContext であったり、Pekko Stream なら Pekko) で実行するようなコードを陽に書いたことがある?
Kory.icon:
chen.icon: 記憶の限りないかもしれない
windymelt.icon: あまりなさそう
hsjoihs.icon: カラオケボックスでこりーさんからたくさん話を聞いて、まあ納得はした
cats-effect などのライブラリが提供している並行データ構造 (MVar や Queue) などを使った経験はある?
Kory.icon:
chen.icon: 初めて聴いたかもしれない
windymelt.icon: QueueとかHotswapとかで遊んだことはある
hsjoihs.icon: Scala 書いたことなし。並行性に触れ合ったことなし。
cats-effect の Ref に準ずるものは使ったことがある?
Kory.icon:
chen.icon: Ref がないと生きていけない
windymelt.icon: たま〜にあるけどそんなに使ったことがないかも
hsjoihs.icon: "safe concurrent access and modification of its content" なるほどね。本当に並行性に触れ合ってこない人生をしてきたため。
fs2.Stream に直接生えている (インスタンスメソッドや、object Stream に生えている) API はどれくらい使った & 眺めたこと (お目当てのメソッドを「あるはずだけど~~」って言って API ドキュメントを上から順に見ていく、などの経験) がある?
Kory.icon:
chen.icon: 眺めることは割とある、が触ったことあるのは一部くらいかも
windymelt.icon:
なんか面白そうなメソッドが生えていたらちょっと触ったことあるかも
雑に眺めて発掘することはあるけれど、ごくprimitiveなやつ(unconsとか)は難しくて無視しがち
hsjoihs.icon: Scala 書いたことなし
fs2.Pull に生えている API は (e.g. s.pull.uncons とかして得られる Pull のメソッドであったり、object Pull のメソッドを) どれくらい使ったことがある?
Kory.icon:
chen.icon: 程々には....?くらい ↓ はパット見で理解できる状態らしい
windymelt.icon: 少し調べてフィボナッチPull自作くらいはした
hsjoihs.icon: Scala 書いたことなし。いま ↑ を読んで最低限の理解を生やした
StateT や WriterT などのモナドトランスフォーマーを使ったことがある?自分で定義したり、それらがどういうものなのかを説明できる?
Kory.icon:
chen.icon: 使うのは結構ある、cats-mtl とかもある、ただ自分で作ったことはない
windymelt.icon: たまにある。あるけど自作はしないかな〜
hsjoihs.icon: 使ったことがある。自分で定義したことがある。書いたコードをリファクタリングしている最中に暗に埋まっている StateT を掘り出したりといった。
言語処理系についての基礎的 (basic、という意味ではなく、foundational という意味) な知識の程度について
何らかの言語のインタプリタを作るにはどうすればよいか (どういうデータ型をどう定義して、どういう関数を定義すれば、それがインタプリタと呼べるのか) というのはわかる (自分が触れたことがある具体例などを書いてほしい)?
chen.icon: パーサーから AST 作って AST を解釈するくらいの感じならやったことある、Brainf*ck も一応ある
「解釈する」ってどういう感じでしょう?Kory.icon
AST をもとに JSON みたいなデータ構造のやつを潜ってく感じ....?(JSONPath とか知ってるならそれみたいな奴です
プログラムをパースして実行する、という感じのものではなく、構造化データをパースしてその構造を inspect する、といった感じのものという理解で良い?Kory.icon → だいたいそう chen.icon
hsjoihs.icon: インタプリタを C で書いたり Haskell で書いたり Rust で書いたりしたことがあるので、ポインタスパゲッティ&ラベルスパゲッティでの実装も代数的データ型での実装もサクッと書ける気がする。正月に新規に REPL を作った。
何らかの言語のコンパイラを作るにはどうすればよいか?というのはわかる? (上の設問同様、自分が触れたことがあるものを並べてみてほしい)
chen.icon: コンパイラはないかも
hsjoihs.icon: Brainf*ck にゼロコスト抽象を入れた言語のコンパイラを Haskell で書いたり、C 未経験の状態から C で C コンパイラを書いたり、Rust で「直に ELF にコンパイルする C コンパイラ」を書いたり、1500 行でセルフホストできる C コンパイラを書いたりしたことがある。C コンパイラの作り方を指南して💰を頂いたりもしている。
windymelt.icon: ぜんぜんわからない!!
非同期ランタイム (Scala の Future + ExecutionContext 周りや IOFiber、Rust の tokio::runtime、Kotlin の coroutine Context、JavaScript の Promise、Go の goroutine、Lua の coroutine、Java 21 の Project Loom の軽量スレッド) の実装側の話はどれくらいわかる?例えば、実装ソースコードを見ながら、どのような概念が持ち出されてきているかを他人に説明できる?仮に一から作るとしたら、どのような作業が必要かというのがわかる?
chen.icon: IOFiber 周りはちょっとだけわかる気がするけど説明できるほどの言語化は出来ないかも
言うても ThreadPool 的なのが自作されてるとか、Fiber が実行する命令が blocking な時に Thread をいい感じに他の Fiber に渡してくれるみたいな程度の理解 (説明が下手だけども)
windymelt.icon: N:Mみたいなメカニズムがあるみたいな話題はしってる
hsjoihs.icon:「OS は如何にしてタイムスライスとかで CPU 時間というリソースをプロセスに対して配っているか」という特殊例に特化した説明だけは追っている。ゆえにイベントループとか stackful coroutine とかは知っている。あと、ECMAScript の仕様書リーディングが好きなので、マイクロタスクとかの話も一応知識としては知っている。2 桁人が同時にアクセスしてくるようなサービスを作っていないため、実践した経験には乏しい。
メモリフェンスなどの知識はありますか?(JVM では volatile 周りの話とか、happens-before 関係で定義されるメモリの変更の可視性についてとか)
chen.icon: なんやそれって感じ
hsjoihs.icon ralfj.de と江添亮に鍛えられた
windymelt.icon 絶無
fs2.Stream / cats-effect についての知識
cats.effect.IO の定義を読んだことはある?
chen.icon: 本質的には AST で実行時はそれを解釈してるみたいなふんわりとした認識
windymelt.icon あんましない
hsjoihs.icon カラオケボックスで読んだ気がする
fs2.Pull の背後にあるメンタルモデルはどれくらい知っている?
この説明を見てうなずける?
chen.icon: 首がもげる程度には
よくよく考えたら終端の表現は知らないかも だいたい理解した
hsjoihs.icon 「まあそうですよね」となるけど、気づいてない非自明ポイントが埋まっている可能性を排除するものではない
windymelt.icon なんか書こうと思ったら俺が書いたやつだった
その他
ScalaMatsuri 2024 での Eff の発表 (link)、見た?後半のトランスパイラ周りの話ってどれくらい把握できている? chen.icon: 9 割くらい理解してる、というか程々に理解するまで実験台にされた
windymelt.icon わからせ(本来の意味)だ
hsjoihs.icon 弊社に対する大規模なサイバー攻撃とかのせいで半分ぐらい記憶が吹っ飛んでいる可能性を否定するものではないが、今朝見返したのでまあ多分大丈夫
windymelt.icon 見た
そうだね〜くらいの気持ちで納得はしたような状態。完全に理解™してはいない 旅程
もともと考えていたもの
1. cats-effect の IO の定義
2. fs2.Stream から cats.effect.IO への変換と cats.effect.IO から scala.concurrent.Future への変換がどのような抽象化レイヤで動作しているのかの確認
これはどっかに書いてあるわけではないので僕が話すのがよさそう
3. cats-effect の IOFiber の実装 (つまり、cats.effect.IO から Future などへどう変換するか、の中身を見る
4. fs2.Stream の API をざっと学んで Stream への簡易的な直感を生やす
5. fs2.Pull の定義を学んで、Stream の各種コンビネータがどのような Pull によって表現されているのかを把握する (fs2 を普通に使うのならここで止まっていい)
6. fs2.Pull#compile の大まかな構造を (もし IOFiber を履修しているなら、それと比べつつ) 把握する。stepLeg / uncons 以外のすべてのケースがどう処理されるかを (Scope の取り扱いを含めて) 理解する
7. stepLeg / uncons がどう compile されるかを理解し、Scope との相互作用が stepLeg / uncons の間でどう違うのかを完全に理解する
懸念: 1~3 がめっちゃ重い
なぜこういう道筋を最初組んでいたかというと、
4~7 は多分変えようがない (7 が最終目標、7を倒すためのその他簡単なところが 6 においてあり、6 をすんなり理解するために 3 をおいており、その他 6 ← 5 ← 4 や 3 ← 2 ← 1 という依存関係がある)
仮にこれでやるとしたら、一セッションを 2 時間として、
(1) + (2) が同じ1セッション
(3) が 4 セッションくらい??
(4) + (5) が 1.5~2 セッション程度
(6) は (3) をきっちり押さえてさえいれば 1 セッション
(7) は Kory.icon もわかってないので謎 3 セッションは行かないはず
くらい掛かる
結論: (3) を落とすと (6) がその分高負荷になってしまうので、これで頑張ってみるでいいんじゃないか
次回:10/22(火) 20:00~