ClojureScriptにAsync/Awaitを導入
(clojurescript.org)- ClojureScript 1.12.145 では、
^:asyncヒントを付けた関数を JavaScript のasync functionとして出力するようにコンパイラが変更された awaitによって Promise の値を待つ ClojureScript 関数を書けるようになり、JavaScript との相互運用性が向上した- テストでも
^:asyncを使用でき、awaitで非同期関数呼び出しの結果を検証できる - 最近の Clojure アンケートでは、async functions のサポートが JavaScript 相互運用性に関する ClojureScript 改善要望の中で最も高い比率を占めた
- 最新のブラウザ API や人気ライブラリを扱う一般的なケースで 追加の依存関係 を導入する必要が減り、変更の全一覧は ClojureScript changelog の 1.12.145 項目 で確認できる
^:async と await の使用
- ClojureScript 1.12.145 では、
^:asyncヒントを付けた関数を JavaScript async function として出力するようにコンパイラが変更された - ClojureScript が ECMAScript 2016 を対象にするようになったことで、JavaScript 相互運用性の改善領域を慎重に選べるようになった
awaitを使ってPromiseの値を待つ ClojureScript 関数を書けるようになった(refer-global :only '[Promise]) (defn ^:async foo [n] (let [x (await (Promise/resolve 10)) y (let [y (await (Promise/resolve 20))] (inc y)) ;; not async f (fn [] 20)] (+ n x y (f))))- テストでも
^:asyncを使用でき、awaitで非同期関数呼び出しの結果を検証できる(deftest ^:async defn-test (try (let [v (await (foo 10))] (is (= 61 v))) (let [v (await (apply foo [10]))] (is (= 61 v))) (catch :default _ (is false))))
背景と変更一覧
- 最近の Clojure アンケートでは、async functions のサポートが JavaScript 相互運用性に関する ClojureScript 改善要望の中で最も高い比率を占めた
- 今回の改善により、最新のブラウザ API や人気ライブラリを扱う一般的なケースでは追加の依存関係を導入する必要が減った
- 修正・変更・改善の全一覧は ClojureScript changelog の 1.12.145 項目 で確認できる
- ClojureScript 1.12.145 にはコミュニティメンバーの Michiel Borkent が貢献した
1件のコメント
Hacker Newsのコメント
borkdude がこのスレッドを立てていて、今回のリリースの貢献者にも名前が載っているのを見た。
かなり前から async/await サポートに反対する論拠はおおむね二つで、CLJS コンパイラ全体に深い変更が必要だという点と、Promesa のようなライブラリのマクロで似たような利便性が得られるという点だった。
ほかにも core.async を使えばよいとか、式指向言語は async/await と相性がよくないという主張もあったが、フォーラムで繰り返される主流の議論というよりは個人の見解に近かった。
Clojurians Slack で borkdude は、サポート追加が非現実的だと確信しているわけではないと言っていたことがあり、結局時間をかけて実装してくれたようで本当に感謝している。
面白い事実として、ClojureScript は JavaScript 自体に async/await が入るよりずっと前から core.async ライブラリで非同期パラダイムをサポートしていた。
これは今回のリリースの価値を貶めたいわけではまったくなく、依存関係にライブラリを一つ追加するだけでホスト言語にまだ存在しない新しい言語機能を使えたというのがすごい、という話だ。Clojure は本当にすごい。
David Nolen の講演を見て知った気がする。
その後はフロントエンドで JavaScript を最小限しか使わない方向へ移っていて、SSE は一方向なのでそこが美しい。最近いろいろな言語圏の開発者が SSE に関心を持っているのを見るとうれしい。
David Nolen の最近の講演「A ClojureScript Survival Kit」も良かった: https://youtu.be/BeE00vGC36E
David “Swannodette” Nolen が ClojureScript と core.async の初期からしてきた仕事には、どれだけ感謝しても足りない。この講演で特に驚いたのは、彼が ClojureScript を捨てて、サーバー側の純粋な Clojure と Server-Sent Events、そしてごく少量の JavaScript だけを使う方向にも実際に期待感を示していることだ。
実際のデモは 26:30 あたりから始まる。クライアントで動く Web アプリのリソース使用量を見せたあと、同じ Web アプリをサーバーで実行し、SSE でクライアントに一方向にプッシュする様子を見せるのだが、リソース使用量がほぼ 0 に近くなっていてかなり強烈だ。
すべてのケースに合うわけではないが、最小限の DOM 変更ライブラリを使うことで Web アプリと状態を推論しやすくなった。以前は Clojure 用 REPL と ClojureScript 用 REPL を両方立ち上げ、双方向トラフィックや再現しにくい状態をたくさん扱う必要があったが、今はずっと速く、再現も簡単だ。
JavaScript の生成物が大きくなり、内在するエラーモデルがなく、問題が起きると読みにくくデバッグしにくい状態機械コードに変換される。
しかも
goマクロは自分の S 式の外側のコードを変換できないので、関数が不必要に大きくなりがちだ。Cognitect の誰かが言っていたように、「core.async は美しいでたらめ」だ。
最近突然ソーシャルで Clojure/ClojureScript を以前よりよく見かけるのが意外だ。
2012 年ごろに数年間仕事で使っていたが、ほかの多くの人と同じく JVM を離れて型付き関数型言語へ移った。
最近関心が高まっているのはエージェント型コーディングのためだろうか。型チェックもなく、厳密な構文エラーや予約語も少ないのでコードを流し読みしやすいからだろうか。S 式の復活が来ているのだろうか。
私の知る本気の Clojure コードベースはどれもテストスイートにかなり投資しているので、AI にそのテストスイートを最も効果的に使う方法だけ教えれば、かなりうまく走らせられる。
同僚の中にはエージェントに REPL と対話させている人もいて、毎回の起動コストを払わずに済むのでさらに速いらしい。私は面倒くさがりなのでそこまではしていないが、それでも今の時点で十分速い。
Clojure は邪魔になる要素が少ない。
falseとnil以外はすべて真で、演算子優先順位表はなく、コア言語が不変・永続データ構造を標準でサポートしている。すべてが式であり、演算子と式が入り混じった構造ではない。
map、reduce、filterは組み込みで、普通のコードで当然のように使われる。10 年前に書いた Clojure コードは今日でもたいてい動く可能性が高く、エコシステムと言語設計者たちはコードを壊すことを禁忌のように扱っている。
これまで使った言語の中で、アイデアを表現する自由度が最も高く、頭痛の種が最も少なかった。事実上の逆方向デバッガである Flowstorm も、プログラミングにおける夢のようなツールだ。
満足して過ごしたいなら本当に良い言語だ。逆に、利用者の多くはそれを当然のものとして受け止めているので、あまり騒がない傾向がある。
商業的に Clojure を使っているプログラマの中には、言語をよく理解できずあまり幸せではない人も多い。自分で選んだわけではないか、まだ準備ができていないことが多く、Clojure を使う前に他の言語で嫌だった点を 10 年くらい経験しておく必要があったのだと思う。
Clojure の作者 Rich Hickey のソフトウェア関連動画は有名で影響力もあるが、だからといって同僚たちがそれを見ていたり気にしていたりするわけではない。
大規模な型なし Python コードベースを AI と扱うのは大変だった。テストでカバーされていない部分が壊れていないか確認する作業があまりに退屈だ。
型システムは強いほど良い。また、AI モデルはコードで学習されるので、言語がより一般的であるほど性能も良い可能性が高い。ClojureScript は良いが主流言語ではないので、JavaScript より AI の性能は落ちると思う。
結局、AI を意識するなら型付き言語を選ぶか、動的言語でも型ヒントのあるものを選ぶほうがよい。
これは本当に大きい。Jank が発表されて以来、Clojure エコシステムでこれほど期待されたことはなかった。
フロントエンドで JavaScript の代替が単なるニッチを超えて本当に定着してほしい。
ClojureScript のようなものを使ってみたいが、個人のサイドプロジェクト以外でどこに使えるのか想像しにくい。すでにバックエンドが Clojure の組織なら導入しやすいのかもしれない。
本番では使ったことはないが、いくつかのサイドプロジェクトや家族向けのものはデプロイした。ClojureScript の React ラッパーである Reagent は、正直 React 自体より筋が通っていると感じた。
Hiccup で HTML を作り、コンポーネントは Hiccup DSL 内の単なる関数で、この DSL 自体も実質的にはリストなので、結果はとてもきれいだ。静的なものは静的に見え、動的なものは明確に動的に見え、普通の React より魔法がずっと少なく感じられた。
良くないと感じたのは、NPM で見つけた非関数型コンポーネントを使おうとしたときだった。致命的ではないがコードが汚くなる。ラッパーで改善はできたが、一部の JS ライブラリは cljs から使うと初期状態がかなり雑然としている。
コミュニティもとても親切で成熟している。
まず個人用スクリプトから置き換えて感触をつかみ、その後で利点を実感するのがよい。どんな場合でも常に優れているわけではないが、あとで人から助言を求められるかもしれないので、自分自身が十分に確信を持っている必要がある。
見慣れない技術を持ち込むときは、重要度の低いものを選んで書き直し、そのまま置いておく戦略がよい。問題が起きても戻しやすいし、人々が気に入り始めたら少しずつ広げればいい。
以前 .NET 組織に F# をこっそり持ち込んだときも、重要度の低いテストから F# で書き始めた。
https://blisswriter.app/
https://blog.nestful.app/p/how-we-dropped-vue-for-gleam-and
cljs を長く追っていないが、もともとは「JavaScript 上の Clojure」くらいの紹介だったと記憶している。少なくとも Rich は最初そう説明していた気がする。
できるだけもう一つのランタイムに近いものにする意図だと理解していた。
ところが今回の変更は cljs にしかない機能を追加するように見えるし、
awaitはすでにclojure.coreのキーワードなので Clojure 自体とも衝突する。二つの実装が時間とともに分岐したのか、それともこの機能が利用者にとってその差を受け入れるだけ重要だったのか気になる。
追加ライブラリを入れずに JavaScript 相互運用を扱えるという点で重要だ。
長らく欠けていた機能なので、今回のリリースはかなりうれしい。
async/await 関数を CSP で包むほうが、より良い扱い方に思える。Clojure にはすでにもっと良いパターンがあった。
core.async がなくなるわけではなく、async/await が Promise ベースの実装よりうまく合うなら、core.async の
.cljs部分も更新されるかもしれない。今回の新しい関数ヒントが入っても、そのやり方が消えることはないと思う。
https://clojurescript.org/guides/promise-interop#using-promi...
これをどう受け止めればいいのかよく分からない。core.async の要点の一つは、こういうものを全部チャネルへ押し込むことではなかったかと思う。
JavaScript 風の
asyncキーワードを持つことが本当にアップグレードなのか確信が持てない。必ず使う必要はなく、今まで通り core.async も使える。最近の ClojureScript アンケートで最も多く要望されていた機能でもあった。