7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

今年もアドベントカレンダーの季節がやってきました。この1年のClojure界隈の動きを振り返ってみましょう。

Clojure 1.12

今年9月、2年越しで開発が進められていたClojure 1.12がついにリリースされました。
このリリースに至るまでには、alphaリリース12回、betaリリース2回を要し、最近では最も多くの変更が取り入れられたリリースとなりました。また、alphaリリースで提案された新機能がコミュニティからのフィードバックによって覆るといった経過を何度か辿っており、近年のリリースの中では最も「難産」なリリースでもありました。

Clojure 1.12の主要な変更を改めてまとめると以下のようになります:

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/1doubleの二次元配列は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の仮想スレッドサポートが進められることが告知されていて、なかなかエキサイティングな年になりそうです。注目していきましょう。

7
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?