今年もアドベントカレンダーの季節がやってきました。この1年のClojure界隈の動きを振り返ってみましょう。
Clojure 1.12
今年9月、2年越しで開発が進められていたClojure 1.12がついにリリースされました。
このリリースに至るまでには、alphaリリース12回、betaリリース2回を要し、最近では最も多くの変更が取り入れられたリリースとなりました。また、alphaリリースで提案された新機能がコミュニティからのフィードバックによって覆るといった経過を何度か辿っており、近年のリリースの中では最も「難産」なリリースでもありました。
Clojure 1.12の主要な変更を改めてまとめると以下のようになります:
-
add-lib
- REPLを立ち上げ直さずにライブラリ追加を可能に
- Java interopの強化
- 後述
-
コレクションの効率的なパーティショニング
-
partitionv
,partitionv-all
,splitv-at
の追加
-
- Java
Supplier
サポート-
IDeref
の実装クラスがSupplier
の実装として使えるように
-
-
Java Streamのサポート強化
-
stream-seq!
,stream-reduce!
,stream-transduce!
,stream-into!
の追加
-
-
仮想スレッドサポートに向けた修正
- 仮想スレッドをブロックしてしまう恐れのある箇所の修正
Java interopの強化以外の項目については過去にまとめた記事があるので、詳しくはそちらも参照して下さい。ここでは、今年大きく仕様が変更されたJava interopの強化に関する機能について改めて見ていきます。
Java interopの強化
1.12のリリースに向けて、最後まで調整が重ねられていたのがJava interopの強化でした。これは、具体的には以下の4つの変更からなるものです:
- 関数型インタフェースのサポート強化
- メソッド値とクラス修飾されたメソッド
- 引数タグメタデータ
- 配列型の新記法
以降、これらについて個々に見ていきます。ただし、これらについても過去にまとめた記事がありますので、変更導入のモチベーション等の背景情報についてはそちらに譲ります。ここでは、それらの記述は最小限に留め、過去記事からの差分となる部分を中心に紹介したいと思います。
関数型インタフェースのサポート強化
Javaのメソッドで引数に関数型インタフェースを期待するものに、Clojureの関数がそのまま渡せるようになる、という変更です。
たとえば、関数型インタフェースを多用していることで有名なJavaのStream APIをClojureから使うには、従来では以下のようにreify
等を使って関数型インタフェースの実装を定義してやる必要がありました。
(-> (IntStream/range 0 10)
(.filter
(reify IntPredicate
(test [_ x]
(even? x))))
(.map
(reify IntUnaryOperator
(applyAsInt [_ x]
(* x x))))
(.forEach
(reify IntConsumer
(accept [_ x]
(println x)))))
Clojure 1.12からは、これを以下のようにメソッドの引数にClojureの関数を直接渡せるようになりました:
(-> (IntStream/range 0 10)
(.filter even?)
(.map #(* % %))
(.forEach println))
メソッド値とクラス修飾メソッド
Clojure 1.12ではJavaのメソッドをファーストクラスの値として扱えるようになりました。
たとえば、JavaのLong
クラスはparseLong
というstaticメソッドを提供していますが、これをClojureのmap
関数に渡して使うことができるようになります:
(map Long/parseLong ["1" "2" "3"])
;=> (1 2 3)
実際には、Clojureコンパイラが値としてのコンテキストでメソッドが使われているのを見つけると、それを包むような関数を自動的に生成するため、Clojureでファーストクラスの値として扱えるようになります。このファーストクラスの値としてのメソッドを メソッド値 (method value) と呼びます。
上の例ではstaticメソッドを呼び出していますが、コンストラクタやインスタンスメソッドについてもメソッド値をとることができます。コンストラクタやインスタンスメソッドのメソッド値を得るには、1.12で新しく追加された クラス修飾メソッド (qualified method) の記法を使います。
クラス修飾メソッドは以下のような形式をとります:
クラス修飾メソッド | |
---|---|
staticメソッド | <class>/<method> |
コンストラクタ | <class>/new |
インスタンスメソッド | <class>/.<method> |
コンストラクタのメソッド値を得るには、<class>/new
という形式のクラス修飾メソッドを使います:
(repeatedly 3 Object/new)
;=> (#object[java.lang.Object 0x7c853486 "java.lang.Object@7c853486"]
; #object[java.lang.Object 0x174e1b69 "java.lang.Object@174e1b69"]
; #object[java.lang.Object 0x1046498a "java.lang.Object@1046498a"])
インスタンスメソッドのメソッド値を得るには、<class>/.<method>
という形式のクラス修飾メソッドを使います。たとえば、String
クラスのtoUpperCase
メソッドを関数の引数に渡す場合は以下のように書きます:
(map String/.toUpperCase ["a" "b" "c"])
;=> ("A" "B" "C")
ちなみに、これらのクラス修飾メソッドは、値としてのコンテキストでメソッド値を得るためだけでなく、メソッドの直接呼び出しにも使うことができます
;; コンストラクタの呼び出し
(Object/new) ;=> #object[java.lang.Object 0x73a19967 "java.lang.Object@73a19967"]
;; インスタンスメソッドの呼び出し
(String/.toUpperCase "a") ;=> "A"
引数タグ (param-tags) メタデータ
メソッド値を使う際に、呼び出すメソッドやコンストラクタにオーバーロードが複数あってどのオーバーロードを呼び出そうとしているかが曖昧になるときは、通常のメソッド呼び出しのときの同様にリフレクションが発生します:
(set! *warn-on-reflection* true)
(map Math/abs [-1 0 1])
;; Reflection warning, NO_SOURCE_PATH:1:1 - call to static method abs on java.lang.Math can't be resolved (argument types: unknown).
;=> (1 0 1)
こういった場合にオーバーロードを解決するために、新しく 引数タグ (param tags) というメタデータが追加されました。引数タグは、^[Type1 ... TypeN]
という形式のメタデータでメソッドの引数型の組Type1
, ..., TypeN
を指定することでオーバーロードされたメソッドを特定します。
Math/abs
の例では、以下のように引数タグでメソッドの引数型を指定することでオーバーロードを解決できるようになります:
(map ^[long] Math/abs [-1 0 1])
;=> (1 0 1)
(map ^[double] Math/abs [-1 0 1])
;=> (1.0 0.0 1.0)
コンストラクタやインスタンスメソッドについても同様に引数タグをつけてオーバーロードを解決できます:
(map Long/new [1 2 3])
;; Reflection warning, NO_SOURCE_PATH:1:1 - call to java.lang.Long ctor can't be resolved.
;=> (1 2 3)
(map ^[long] Long/new [1 2 3])
;=> (1 2 3)
(map ^[String] Long/new ["1" "2" "3"])
;=> (1 2 3)
(let [f String/.getBytes]
(f "abc" "UTF-8"))
;; Reflection warning, NO_SOURCE_PATH:1:1 - call to method getBytes on java.lang.String can't be resolved (argument types: unknown).
;=> #object["[B" 0x778db7c5 "[B@778db7c5"]
(let [f ^[String] String/.getBytes]
(f "abc" "UTF-8"))
;=> #object["[B" 0x33db72bd "[B@33db72bd"]
また、オーバーロードは複数存在していても、引数の数だけ指定すればオーバーロードを一意に特定できるような場合には、プレースホルダーとして型のかわりに_
(アンダースコア)を指定することもできます。
たとえば、Long/toString
には引数の数が1つのオーバーロードと2つのオーバーロードが存在します。ここで、引数の数が1つのオーバーロードを呼ぼうとする場合、以下のように引数型を明示的に指定することもできますが、
(map ^[long] Long/toString [1 2 3])
;=> ("1" "2" "3")
以下のように、引数の数が1つであることだけを示すことでオーバーロードを解決することも可能です:
(map ^[_] Long/toString [1 2 3])
;=> ("1" "2" "3")
なお引数タグは、通常のメソッド呼び出しに対しても指定できるようになり、従来オーバーロードを解決するのに引数に対して型ヒントを与えていたのを置き換える役割で使うことができます:
(^[long] String/valueOf 42)
;=> "42"
(^[boolean] String/valueOf true)
;=> "true"
配列型の新記法
1.12では、Javaの配列型のサポートも強化されました。
これまで、配列型の型ヒントを書く際には、Javaの内部的な表現を使って[Ljava.lang.String;
(String
の配列)や [[D
(double
の二次元配列)のように書く必要がありました。1.12からはより簡潔で分かりやすい配列型の表現が追加されます。具体的には、<type name>/<dimension>
のように型名に /
を挟んで配列の次元数を後置することで配列型を表現できるようになります。
この記法によって、String
の配列はString/1
、double
の二次元配列はdouble/2
と書けるようになります。この記法は、型ヒントや引数タグとして使える他、値としてのクラスを表現するのにも使えます:
(class? String/1)
;=> true
(instance? long/1 (long-array [1 2 3]))
;=> true
(let [^long/1 arr (long-array [1 2 3])]
(alength arr))
;=> 3
;; 引数タグでも使える
(^[double/1 double] java.util.Arrays/binarySearch (double-array [1.0 2.0 3.0]) 2.0)
;=> 1
Clojarsでライセンス情報が必須に
Clojarsでは今年初頭から、デプロイされるすべてのライブラリに対してライセンス情報が付与されていることを必須としました。
これは、ライブラリのユーザが依存ライブラリのライセンス情報を収集し、ライセンス違反がないかを検証しやすいようにするための施策で、昨年9月から始まった新規のライブラリに対するライセンス情報必須化がさらに対象を広げたものです。
新しいライブラリをClojarsでリリースする場合や既存ライブラリの新しいバージョンをリリースする場合は、ライブラリのPOM中にある<license>
タグに適切なライセンス情報が書かれている必要があります。
ただし、Leiningenやdeps-new等のツールを使って組み込みのプロジェクトテンプレートでプロジェクトを作成している場合は、すでにライセンス情報がPOMに書き出されるようにセットアップされているので特に対応の必要はありません。ライブラリのPOMを生成するのにtools.build
を使っている場合は、ライセンス情報がPOMに書かれているか確認する必要があります。
- 参考
test.regression
Clojure 1.12の開発が進められてる最中の今年2月頃からtest.regression
という新しいリポジトリがGitHubのClojure organization下に準備し始められました。
test.regression
は、Clojure自身の開発によるリグレッションを検出するために、Clojureで書かれた様々なOSSライブラリやアプリケーションから構成されたコーパスです。同様のものにはRustのcraterやmypyのmypy_primerが存在します。
Clojure 1.12の開発過程では、コンパイラに大きな変更が加えられ、コーナーケースの挙動変更によって既存コードが動かなくなる問題がコミュニティから何度か報告されることがありました。test.regression
はそんな状況の中、新しい変更が既存のコードベースを壊していないことをより高い確度で保証するために用意されました。
2024年12月現在、190程度のOSSとほぼすべてのClojure contribライブラリ、そしてDatomic Proがこのリポジトリに含められています。リポジトリに含めるOSSがどういう基準で選定されているかについては明らかにされていませんが、テスト対象は定期的に更新されていて、Clojureやその周辺の開発にも役立てられていくものと思われます。
おわりに
今年はまずClojure 1.12が無事リリースできてよかった、というのに尽きる1年でした。今後もcore.asyncの仮想スレッドサポートが進められることが告知されていて、なかなかエキサイティングな年になりそうです。注目していきましょう。