Clojure 1.12.0 リリース
(clojure.org)- Clojure 1.12.0 は Java 8 バイトコードを維持しつつ、今後のリリースで最小 Java 互換性とバイトコード基準を新しい Java LTS に移す前の、最後の Java 8 基準リリースになる予定
- JDK 21 の 仮想スレッド 環境で、
lazy-seqとdelayがsynchronizedの代わりにロックを使うようになり、ブロッキング I/O が実スレッドを固定してしまう状況を減らす - REPL では JVM を再起動せずに
add-lib、add-libs、sync-depsでライブラリを追加できるが、この機能は 開発中の対話的利用 に限定される - Java 相互運用性が拡張され、メソッド値、
:param-tags、配列クラス構文、関数型インターフェース変換、Supplier、JavaStream処理関数が追加 - パフォーマンスと互換性の面では、
PersistentVectorspliterator、効率的なdrop/ パーティション処理、Var interning ポリシーの強化、CVE-2024-22871 の修正、Java シリアライズ識別子の整理が含まれる
Java 8 互換性、セキュリティ、シリアライズ整理
- Clojure 1.12.0 のダウンロードと利用情報は Downloads ページで確認できる
- Java 8 基準 は今回のリリースでも維持される
- Clojure 1.12 は Clojure 1.10、1.11 と同様に Java 8 バイトコードを生成する
- 今後のリリースでは、バイトコードと最小 Java 互換性をより新しい Java LTS リリースへ移行する予定
- JDK 21 の 仮想スレッドのピン留め 問題が緩和された
- 1.12 より前では、
lazy-seqとdelayは一度だけ実行されることを保証するため、ユーザーコードをsynchronizedブロック内で実行していた - JDK 21 時点では
synchronizedはまだ協調的ブロッキングに参加しないため、そのコードがブロッキング I/O を行うと実スレッドを固定することがある -Djdk.tracePinnedThreads=fullを使うと、JDK 21 がこの状況について警告を出す場合がある- 1.12 では
lazy-seqとdelayはsynchronizedブロックの代わりにロックを使用する
- 1.12 より前では、
- セキュリティ修正として CVE-2024-22871 が取り込まれており、関連する勧告は GHSA-vr64-r9qj-h27f にある
- Java シリアライズ関連クラスの serialVersionUID が明示的に設定された
- Clojure のデータ型は Clojure 1.0 から Java シリアライズインターフェースを実装してきた
- Java シリアライズは、クラス名、型階層、シリアライズ対象フィールドから生成される識別子が逆シリアライズ時に一致している必要がある
- Clojure はバージョン間のシリアライズ一貫性を保証しないが、必要以上に互換性を壊さないよう、今後の制御性を高める変更を適用した
- 依存関係もあわせて更新された
spec.alphaは 0.5.238 に更新core.specs.alphaは 0.4.74 に更新
REPL でライブラリとツールを扱う機能
- 開発中には JVM を再起動せずにライブラリを追加したい状況がある
- 実験的な評価
- プロジェクトに既知の依存関係を追加
- 特定の作業のためにライブラリを追加
- Clojure 1.12 は、REPL の状態を失わずにライブラリを追加する 新しい関数群 を提供する
- これらの関数は 開発中の REPL 利用 のみを意図している
- 本番コードをビルドし維持する正しい方法は、引き続き
deps.ednを使うこと - 3 つの関数はいずれも
*repl*が true にバインドされているかを確認する clojure.main/replはこのフラグを自動的にバインドするclojure.mainREPL では新しい関数群がuser名前空間に自動で refer される- 他の REPL では
(require '[clojure.repl.deps :refer :all])が必要な場合がある
- 本番コードをビルドし維持する正しい方法は、引き続き
- ライブラリ解決とダウンロードは tools.deps が担当する
- 開発中のプロジェクトのクラスパスに
tools.depsとその依存関係を入れないよう、Clojure CLI 経由で関数を別プロセスから呼び出す新 API も追加された
- 開発中のプロジェクトのクラスパスに
clojure.tools.deps.interop/invoke-toolはツール関数を 別プロセス で呼び出す- ツールのクラスパスは
deps.ednに定義される - ツール依存関係をプロジェクトのクラスパスに追加する必要がない
add-lib機能はinvoke-toolを使って実装されており、ユーザーツールを対話的にビルドまたは呼び出す用途にも使える- 関数実行プロトコルは CLI reference で確認できる
- ツールのクラスパスは
外部プロセス実行 API
- 既存の
clojure.java.shell名前空間に加えて、Java の新しいプロセス情報・制御・I/O リダイレクト API を活用する手段が加わった - Clojure 1.12 は新しい名前空間
clojure.java.processを追加する- Java の新しいプロセス関連 API を活用する
- 従来の方式より使いやすく設計されている
- 主な関数は次のとおり
Java 相互運用性の拡張
- メソッド値 が追加され、Java メソッドを高階関数の中でより直接使えるようになった
- 従来は Java メソッドを
mapなどに渡すには明示的に関数でラップする必要があった - 手動ラップは冗長で、オーバーロードを区別するヒントが必要になったり、副次的なリフレクションやボクシングが発生したりすることがあった
- これからは qualified methods を値位置で通常の関数のように使え、コンパイラが ラップ関数 を自動生成する
- qualified method がオーバーロードのために解決できない場合、コンパイラはリフレクション呼び出しを生成する
- 開発者は
:param-tagsメタデータで、望む単一のメソッドシグネチャを指定できる
- 従来は Java メソッドを
- Qualified method 構文ではクラスとメソッドを明示する
Classname/method: 静的メソッドを呼び出す Clojure 関数値Classname/.method: インスタンスメソッドを呼び出す Clojure 関数値Classname/new: コンストラクタを呼び出す Clojure 関数値- 静的メソッドとインスタンスメソッドを区別するには
Classname/methodとClassname/.method構文を使う必要がある
:param-tagsメタデータはオーバーロードされたメソッドの解決に使われる- 値として使う qualified method はクラス名とメソッド名しか与えないため、オーバーロードメソッドを解決できない
:param-tagsは[tag …]形式のベクタで、各タグが望むシグネチャのパラメータに対応する- オーバーロードされていない型のパラメータには
_プレースホルダを使える :param-tagsを与えると、コンパイラはコンパイル時に単一メソッドへ解決できなければならない- 新しいメタデータリーダー構文
^[tag …]はメンバーシンボルに:param-tagsメタデータを付与する
- 配列クラス構文 が追加された
- Clojure はクラス名シンボルをクラスオブジェクト値や型ヒントとしてサポートしていたが、文字列以外では配列クラス構文を提供していなかった
- これからは
ComponentClass/#dimensions形式のシンボルで配列クラスを参照できる - 例:
String/1,java.lang.String/1,long/2 - コンポーネントクラスは完全修飾クラス名、import 済みクラス、primitive のいずれにもできる
- 配列クラス構文は型ヒントとしても値としても使える
- Java の 関数型インターフェース 相互運用も改善された
- Java の関数型インターフェースは
@FunctionalInterfaceが付き、単一メソッドを持つ - Clojure 関数は arity が一致すれば、関数型インターフェースを受け取る Java メソッド呼び出しに渡せる
- Clojure コンパイラはラムダアダプタを生成し、Clojure 関数を必要な関数型インターフェースへ暗黙に変換する
- ループ内で繰り返しアダプタが生成されるのを避けるには、
letバインディング名にヒントを付けて明示的に強制できる
- Java の関数型インターフェースは
Supplier相互運用も改善された- 値を供給する
Supplierを受け取るメソッドを呼ぶには、以前はreifyでアダプタを書く必要があった - Clojure の
IDeref実装であるdelay、future、atomなどは、これからはSupplierインターフェースも直接実装する
- 値を供給する
Stream 処理とコレクション性能の改善
- Java API が返すことの増えている
Streamを Clojure らしい方法で消費するための関数が追加された- Clojure 1.12 の関数型インターフェース対応とあわせて Stream 相互運用関数 が提供される
(stream-seq! stream) ⇒ seq(stream-reduce! f [init-val] stream) ⇒ val(stream-transduce! xf f [init-val] stream) ⇒ val(stream-into! to-coll [xf] stream) ⇒ to-coll- すべての関数は終端 Stream 操作であり、Stream を消費する
PersistentVectorは Java コレクションの stream 実装で使われる spliterator を直接提供する- spliterator は、より高速な並列走査のために分割できる iterator
PersistentVectorの新しいカスタム spliterator は並列性をサポートし、性能が大きく改善された
drop、nthrest、nthnextとパーティション処理の効率が改善された- CLJ-2713 は、コレクションが逐次走査より効率的に drop できることを示す内部インターフェース
IDropを追加した - このインターフェースは persistent collection や
range、repeatのようなアルゴリズムコレクションに実装されている - 新関数
partitionv、partitionv-all、splitv-atは既存の対応関数より効率的で、realized seq のパーティションではなくベクタのパーティションを生成する
- CLJ-2713 は、コレクションが逐次走査より効率的に drop できることを示す内部インターフェース
Var interning ポリシーの強化
- 名前空間で var を intern することは、aliasing と異なり、すべての参照が同じオブジェクトを得られる 安定した参照 を作ることを意味する
- これまでは intern 済み var が置き換えられるケースが一部あり、1.12.0-alpha1 でポリシーがより厳格になった
- この状況が発生すると、
"REJECTED: attempt to replace interned var #'some-ns/foo with #'other-ns/foo in some-ns, you must ns-unmap first"形式の警告が表示される
- この状況が発生すると、
- このポリシーは、Clojure 1.11.0 で
clojure.coreに新しい関数、特にabsが追加されたことで表面化した問題の根本原因に対処するもの
全変更一覧
- Clojure 1.12.0 の全変更一覧は 公式 changelog にある
1件のコメント
Hacker News の意見
本当に大型リリースで、素晴らしい新機能がたくさんある
個人的には add-libs がいちばん気に入っている。これで、問題再現用の単一ファイルデモや最小例を作れるようになり、実行可能な小さなコード片を共有するハードルが大きく下がった
Java のボイラープレートなしで Java ライブラリもデモできる。REPL でいろいろ試したあと、HN のコメントのような場所にコードを貼り付ければ、誰でも同じ「設定」をそのまま再現して実行できる。リポジトリをクローンする必要もない
Clojure/conj 2024 までこのリリースを遅らせるのだと思っていた。特に根拠があったわけではないが、Clojure 1.10 が Clojure/conj 2021 の頃に出て、Datomic の無料化発表も Clojure/conj 2023 の序盤にあったからだ
それでもまだ spec2 を待っている。今は spec の硬直性を Malli で迂回しているが、Clojure における第一級市民ではない。主な理由はマクロを検査できないことで、これは Clojure コンパイラの設計上意図された部分だ。ただし Malli のスキーマをデータとして操作すれば、schema/select のアイデアはまねできる
関数型インターフェースの変更のおかげで、今では
(defmacro ->Consumer [f] ...)のようなユーティリティマクロを維持する必要なく、関数をそのまま渡せる実際、Clojure core も spec でマクロ呼び出しを検査していて、誤って呼び出すとスタックトレースにその痕跡が見えることがある。別の意味で言っているのか気になる
新機能が山ほど入っているのに既存コードがそのまま動くのが本当に良い。互換性破壊を避けようとする継続的な努力が光っている
Clojure についてもっと知りたいなら、10月23〜25日にバージニア州 Alexandria で開催される Clojure/conj カンファレンスを見るとよい: https://2024.clojure-conj.org
add-libs と sync-deps が入ったのはうれしい。これでセッションをわざわざ閉じる理由はほとんどない、あるいはまったくないように思える
今回のリリースは以前のリリース群とはスコープがかなり違って見えるし、入っている内容も多くて興味深い。ただ、ペースが上がったせいで、数リリース後に絡み合った塊のようにならないことを願う
関数型インターフェースの変更は非常に大きい。Clojure は慎重な相互運用を通じて Java に近い位置にあるときが最も良く、今回の変更は大きな隙間を一つ埋めてくれる
spec はどうなったのか気になる。放棄されたのだろうか?期待できるニュースがあるのか気になる
かなり堅実なリリースに見えるし、Clojure が今も順調なのはうれしい
荒らすつもりではない。選びたいし、工学的には良い判断に見える。だが人気とコントリビューターが急落しているなら、近い将来に足かせになる可能性もある
[0] https://www.clara-rules.org/
既存の開発者を Clojure 開発者へ転換させるのが、これまでになく簡単になった
最初に直面する大きな問題[0]はコードを読むことだが、ChatGPT や Claude のような AI は既存の Clojure コードを説明するのが非常に得意。その結果、開発者のオンボーディングがずっと速くなり得る
[0] 数週間もすれば Clojure を読むのが自然になり、以前は読めなかったという事実すら忘れてしまう
素晴らしい改善が多い。普段手が伸びる主な Lisp 系言語だ