ディスカッション (11件)
この記事は、プログラミングにおける「同期関数」と「非同期関数」の決定的な違いについて、非常に分かりやすく解説した名著です。多くの言語で直面する「関数に色がつく(呼び出し側も非同期にする必要があるなど、制約が伝染する)」という厄介な問題の本質を突いています。非同期処理の沼にハマっているエンジニア必読の考察です。
Goには「関数カラー」なんてものは存在しないよ。実行環境が優秀で、面倒な非同期処理の魔法を全部裏でうまいこと隠してくれるからね。個人的には、並行処理のコードを書くのが本当に楽で助かってる。ただ、似たような別の問題はあるよね。関数がエラーを返すなら、呼び出し元すべてにエラー伝播を徹底させなきゃいけない。context.Contextが必要な関数ならなおさらだ。まあ、何から何まで完璧ってわけにはいかないよね。
「同期コードからFutureを返す関数を呼べない(呼べなくはないけど、そんなことをしたら、将来あなたのコードを保守する人がタイムマシンを発明して過去に戻り、あなたを鉛筆で突き刺しに来るだろう)」という主張だけど、著者は嘘をついているね。しかも、それを面白おかしい例え話でごまかしている。実際、Golangのような言語で代用する場合でも、非同期ロジックが暗黙的になってしまうという別の、しかもずっとひどいトレードオフがある。コードベース全体が、チャネル待ちでブロックされるリスクを抱えることになるんだ。一応コルーチンを適切に使えば緩和はできるけど、結局はasync/awaitほど明示的ではないだけで、同じような「カラー」にまつわるコード情報の管理問題に逆戻りするだけだよ。
もっと多くの言語に代数的エフェクトが必要だね。これがあれば関数カラーの問題は解決する。OCaml 5では導入されていてかなりうまくいっているようだし、OxCamlのような形式で借用チェッカー(borrow checker)のセマンティクスと組み合わせれば、理想的な言語になるんじゃないかな。Rustでも代数的エフェクトを見てみたいけど、残念ながらキーワードジェネリクス(keyword generics)の取り組みは停滞しているみたいだしね。関連して、元Reactメンテナーが書いた代数的エフェクトの解説記事がすごくわかりやすかったよ:https://overreacted.io/algebraic-effects-for-the-rest-of-us/
キーワードをawaitじゃなくてdontawaitにして、逆の意味で使えればよかったのにと思う。非同期関数を使う時の99%は、どれだけ遅かろうと、完了するのを待つ以外にコードがやれることなんてないんだから。もし何らかの理由で、前の処理が終わる前に次の行を実行したいなら、その時だけ「こっちでやるよ」と指定すればいい。「なぜ同期関数から非同期処理をawaitしちゃいけないの?」と思う。もし実行中にスレッド全体がロックされるとしても、99%のケースではどのみちそうなるんだから別に構わないでしょ。
関数カラーがあるのは良いことだよ。何が重要で、書き手が何に注意を払うべきかという言語設計上の意思表示になっているからね。他にも例はあるよ。
- Haskell: 純粋関数と非純粋関数(IOモナド)の見た目が違う。
- Rust: unsafeな関数(やブロック)には特別なマーカーが必要。
この手の記事はあまり好きじゃないな。キャッチーで深そうなタイトルをつけて、気に入らない技術を叩くために持ち出されることが多いから。実際、非同期じゃない関数だって全部「カラー」はあるでしょ。大きなシステムのコードベースなら、特定の条件下や適切なセットアップがないと呼べない関数なんていくらでもある。運が良ければ型定義でそれがわかるけど、どのみち制約から逃れることはできない。制限の緩い関数から厳しい関数を呼ぶのは簡単だけど、逆はそうはいかないしね。それに、明示的なawaitの代替案だって(記事では触れていないけど)課題はある。本質的な複雑さはどうしても残るし、それはトレードオフであって、構文でどうにかできる問題じゃないんだ。
この議論は結局いつも、明示的か暗黙的かという話に帰結する気がする。静的型付け対動的型付けと同じような感じだよね。個人的には、はっきりと明示的な方を支持する側だ。関数の本体や、その先を延々と追いかけなくても、関数について理解できるのが好き。関数シグネチャを見れば、それが整数を返すのか、あるいはIOを実行する可能性があるのかが一目でわかる方がいい。
もちろん、シグネチャに数文字追加しなければならないというコストはあるし、人によってはそれが邪魔になるという感覚もわかる。それに、呼び出し元まで書き換えが必要になることもあるだろうけど、コードは書くことより読むことの方が多いんだから、その程度のコストは無視できるよ。
結局のところ、文脈と個人の好みの問題だ。それに、ある程度の知性も必要だね。動的型付けや非async派の人たちが、プログラミング中にどれだけの情報を短期・長期記憶で保持しているのか見ると感心するよ。残念ながら、自分にはそんな精神的な帯域幅はないからね。とはいえ、asyncキーワードが(カラーなんていう根拠のない話ではなく)有用な情報を伝えているという事実を無視して、「メリットを無視すればこの構文にはメリットがない」と主張するのは不誠実だし、議論として尊重できないな。
もし理解が正しければ、記事で称賛されているGo言語にだって赤や青の関数は存在しているよね。ただ、それが暗黙的に扱われているだけで、コードを読むプログラマーにとっては、どこで何が起きているのかを推測するのがより難しくなっているだけじゃないかな。
いくつか同時に言えることがあると思う。
- async/awaitはコールバックより大幅に改善されている。
- コールバックによる非同期処理は、本来の並行処理を行えない(あるいは行わない)言語設計が生んだ、ずっと泥臭いハックだった。async/awaitは根本の問題を解決せず、その場しのぎで蓋をしているに過ぎない。
- 言語設計の観点からは、スレッドの方がずっとエレガントで強力だ。
- ……とはいえ、スレッドに起因する難解な並行処理バグと戦うよりは、関数のカラーを気にする方がまだマシだな。
これが出た当時に読んだのを覚えているよ。そこから今ここまで来たのかと思うと感慨深いね。