LoginSignup
16
2

Clojure Language Update 2023

Last updated at Posted at 2023-12-01

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

Clojure 1.12

次期バージョンであるClojure 1.12は昨年から開発が進められていますが、年を越して今になってもまだリリースには達していません。昨年の1.12.0-alpha1のリリースからこれまで1.12.0-alpha5までの4つのアルファリリースが加えられています。

今年新たに1.12のリリースに含められるとアナウンスされている変更のうち、主要なものは以下の3つです:

  • add-lib
  • Java interopの強化
  • 仮想スレッドサポートに向けた修正

以下でそれぞれについて詳しく見ていきましょう。

add-lib

add-libはREPLから動的に依存ライブラリを追加するための機能です。

これまでのClojureでは、依存ライブラリを新たに追加する場合、そのライブラリをロードするためにはREPLを起動し直す必要がありました1add-lib機能を使うと、REPLから依存ライブラリを追加し、REPLを再起動することなくライブラリをロードすることができるようになります。

add-libはClojure 1.12からREPLで使えるようになる関数で、追加するライブラリを引数に指定します:

(add-lib 'ring/ring-mock)
;=> [cheshire/cheshire … ring/ring-codec ring/ring-mock tigris/tigris]

戻り値として新たにロードされたライブラリ(指定したライブラリの依存ライブラリ等も含む)のコレクションが返されます。

ライブラリのバージョンをオプショナルに指定することもできます:

(add-lib 'ring/ring-mock '{:mvn/version "0.4.0"})

他にも以下のような関数が追加され、これらもadd-lib同様、REPLから使えるようになります:

  • add-libs: 複数のライブラリを一度に追加する(それらの依存ライブラリも同時に解決される)
  • sync-deps: deps.ednに追加した新たなライブラリをロードする

もともと、Clojureにはクラスローダ(DynamicClassLoader)の機能を使ってライブラリを動的にロードすることはできましたが、依存ライブラリを追加するためには依存ライブラリ自身の依存を解決する機能がClojure本体に欠けていました。

add-lib機能の実現にあたっては、この欠けていた依存ライブラリ解決の機能をClojure CLIに委譲するという方法が採られました。add-lib関数を呼び出すと、内部的にはClojure CLIが別プロセスで起動され、そのプロセス内で依存ライブラリが解決されて、add-libを呼び出したClojureのクラスローダでライブラリをロードする、という仕組みになっています。

また、add-lib実現に伴って、以下のような基盤機能も追加されています:

  • REPLからClojure CLIを起動するためのAPI (CLJ-2760)
  • Java NIO2?ベースのプロセスAPI (CLJ-2759)

add-lib機能はClojure 1.12以降を使っていれば、基本的にはどのように起動されたREPLからでも使えるようになっていますが、上記の通り仕組みのうえでClojure CLIを呼び出しているため、少なくとも環境にClojure CLIがインストールされていなければいけない点に注意が必要です。

参考

Java interopの強化

1.12では、ClojureからJavaの機能を呼び出す、いわゆる Java interop の機能強化も予定されています。強化される予定の機能は以下のとおりで、Java 8で導入されたStream APIやJavaのラムダ式といった機能のサポート等、比較的新しいJavaの機能への対応が主な変更です:

  • 関数型インタフェースのサポート強化
  • Java Streamのサポート強化
  • メソッドがファーストクラスの関数に
  • 引数タグメタデータ
  • 配列型の新記法

関数型インタフェースのサポート強化

Java 8でラムダ式が導入され、最近Javaに追加されるAPIではラムダ式が使えることを前提にしたもの多くなってきました。たとえば、JavaのStream APIは以下のようなラムダ式を多用したインターフェースを提供します:

IntStream.range(0, 10)
    .filter(x -> x % 2 == 0)
    .map(x -> x * x)
    .forEach(System.out::println);

このコードは以下のラムダ式を使わないコードと意味的には等価ですが、mapfilter等のメソッドが具体的にどのような型を引数にとるのかをユーザが明示する必要がないため、ラムダ式を使った方が全体的に簡潔な記述になります:

IntStream.range(0, 10)
    .filter(new IntPredicate() {
        public boolean test(int x) {
            return x % 2 == 0;
        }
	})
	.map(new IntUnaryOperator() {
	    public int applyAsInt(int x) {
		    return x * x;
		}
    })
    .forEach(new IntConsumer() {
        public void accept(int x) {
            System.out.println(x);
	    }
	});

この例の中のIntPredicateIntUnaryOperator等のように、メソッドを1つしか持たないインタフェースは関数型インタフェース(functional interface)と呼ばれ、Javaのラムダ式はこのような関数型インタフェースに対してその実装を与えるための簡単な方法を提供します。

ClojureでJava interopの機能を使ってこのような関数型インタフェースを要求するAPIを使おうとすると、これまではこの冗長なコード例をそのままClojureに翻訳したようなコードを書く必要がありました:

(-> (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)))))

1.12以降では、関数型インタフェースに対してClojureの関数を直接渡せるようになる予定のため、先のサンプルコードは以下のように簡潔に記述できるようになります:

(-> (IntStream/range 0 10)
    (.filter even?)
    (.map #(* % %))
    (.forEach println))

この例で示している通り、even?のようにdefnで定義した名前つきの関数も #(...) などの無名関数も、どちらも関数型インタフェースとして渡すことができます。

参考

Java Streamのサポート強化

1.12では、JavaのStream APIに対するサポートも強化される予定です。

具体的には、以下のようなStreamを消費する(いわゆる終端操作)関数が追加されます:

  • stream-seq!: StreamをClojureのシーケンスに変換する
  • stream-into!: Stream版のinto
  • stream-reduce!: Stream版のreduce
  • stream-transduce!: Stream版のtransduce

Clojureで積極的にStreamを使う必要性はあまりありませんが、JavaのAPIから渡ってくるStreamをClojureで消費するだけの使い方であれば、これらの関数を使う方が便利な場面もありそうです。

参考

メソッドがファーストクラスの関数に

関数型インタフェースを期待するJavaのメソッドにClojureの関数を渡せるようにするのと対をなす変更として、1.12ではJavaのメソッドをClojureの関数として使えるようにする変更も予定されています。

これまでは、Javaのメソッドはファーストクラスの値ではなかったので、たとえばmapのような高階関数に直接渡して使うことはできませんでした:

(map Long/parseLong ["1" "2" "3"])
;; Syntax error compiling at (REPL:1:1).
;; Unable to find static field: parseLong in class java.lang.Long
				  
;; これまではこう書く必要があった
(map #(Long/parseLong %) ["1" "2" "3"])

1.12以降では、Long/parseLongをフォーストクラスの値として高階関数に渡したり、関数の戻り値として使うことができるようになります:

(map Long/parseLong ["1" "2" "3"])
;=> (1 2 3)

上の例はstaticメソッドを呼び出す例でしたが、コンストラクタやインスタンスメソッドも同様にClojureの関数として扱うことができるようになります。

コンストラクタを値として扱う場合には、通常のコンストラクタ呼び出しと同じように、クラス名の後に.を後置して参照します:

(repeatedly 3 Object.)
;=> (#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 name>/<method name>というシンタックスによってメソッドが属するクラスを明示します。たとえば、StringクラスのtoUpperCaseメソッドを値として参照する場合は以下のように記述します:

(map .String/toUpperCase ["a" "b" "c"])
;=> ("A" "B" "C")

ちなみに、この.<class name>/<method name>というレシーバクラスを明示するシンタックスは通常のメソッド呼び出しでも使えるようになります:

(.String/toUpperCase "a")
;=> "A"

参考

引数タグメタデータ

メソッドやコンストラクタを値として扱う際、そのメソッドやコンストラクタがオーバーロードされていると、どのオーバーロードを呼び出せばいいか一意に特定できないためエラーになります。

たとえば、StringクラスのvalueOfメソッドは各プリミティブ型や配列型のために計9つのオーバーロードも持ちます。そのため、String/valueOfをそのまま値として使おうとすると、呼び出すメソッドが特定できずエラーになります:

String/valueOf
;; Syntax error (IllegalArgumentException) compiling at (REPL:0:0).
;; Ambiguous arg-tags for valueOf in class java.lang.String

このような場合を解決するために、新しく引数タグ(arg-tags)というメタデータが追加されました。引数タグは、^[Type1 ... TypeN]という形式のメタデータでメソッドの引数型の組Type1, ..., TypeNを指定することでオーバーロードされたメソッドを特定します。

String/valueOfの例では、以下のように引数タグでメソッドの引数型を指定することでオーバーロードを解決できるようになります:

(map ^[int] String/valueOf [1 2 3])
;=> ("1" "2" "3")

(map ^[double] String/valueOf [1 2 3])
;=> ("1.0" "2.0" "3.0")

コンストラクタやインスタンスメソッドについても同様に引数タグをつけてオーバーロードを解決できます:

(map Long. [1 2 3])
;; Syntax error (IllegalArgumentException) compiling at (REPL:1:1).
;; Ambiguous arg-tags for java.lang.Long in class java.lang.Long

(map ^[long] Long. [1 2 3])
;=> (1 2 3)

(map ^[String] Long. ["1" "2" "3"])
;=> (1 2 3)
				  
(let [f .String/getBytes]
  (f "abc" "UTF-8"))
;; Execution error (ArityException) at user/eval401 (REPL:1).
;; Wrong number of args (2) passed to: user/eval401/dot--getBytes403--405

(let [f ^[String] .String/getBytes]
  (f "abc" "UTF-8"))
;=> #object["[B" 0x514cd540 "[B@514cd540"]

また、オーバーロードは複数存在していても、引数の数だけ指定すればオーバーロードを一意に特定できるような場合には、プレースホルダーとして型のかわりに_(アンダースコア)を指定することもできます。

たとえば、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>-*のように型名に-*を後置することで配列型を表現できるようになります。末尾につけられる*の数が配列の次元数を表し、二次元配列は-**、三次元配列は-***、…というようになります。

この記法によって、Stringの配列はString-*doubleの二次元配列はdouble-**と書けるようになります。この記法は、型ヒントや引数タグとして使える他、値としてのクラスを表現するのにも使えます:

(class? String-*)
;=> true

(instance? long-* (long-array [1 2 3]))
;=> true

(let [^long-* arr (long-array [1 2 3])]
  (alength arr))
;=> 3

;; 引数タグでも使える
(^[double-* double] java.util.Arrays/binarySearch (double-array [1.0 2.0 3.0]) 2.0)
;=> 1

参考

仮想スレッドサポートに向けた修正

1.12にはさらに、Javaの仮想スレッドサポートに向けた修正も含まれます。仮想スレッド (virtual thread)はJava 21で正式に採用された軽量スレッドで、今後利用シーンが広がっていくことが予想されます。

Clojureからも仮想スレッドはJava interopを介してそのまま使うことができます。しかし、遅延シーケンスやdelay等の実装の中で、任意のユーザコードがsynchronizedブロック中で実行される箇所があり、そこでスレッドを長期間ブロックしてしまう可能性がありました。この潜在的な問題を解決するために、遅延シーケンスやdelayの実装がsynchronizedベースの排他制御から仮想スレッドと併用しても問題ないReentrantLockベースの排他制御に書き換えられました。

こちらの修正はすでに1.12.0-alpha5としてリリースされていて利用可能です。この変更によって遅延シーケンスの性能がわずかに悪くなると言われているので注意が必要です。

参考

Rich Hickey、Nubankを退職

今年8月、Clojure開発者のRich Hickeyが、所属するNubankの退職と商用ソフトウェア開発からの引退を発表しました。

引退は長期的に計画されていたもので、Clojureの開発自体には引き続き関わっていくことになるようです。Clojureの開発チームが所属し、Nubankの子会社でもあるCognitectとNubankの関係もこれまでと変わりません。Alex MillerやMichael Fogus等、Clojureの開発にメインで関わっているメンバーは引き続きRich Hickeyと協力してClojureの開発を進め、Stuart HallowayはDatomicの開発をリードしていきます。

「引退」と聞くとネガティブな響きもありますが、アナウンスがあったときのRichの言葉によれば、

Retirement returns me to the freedom and independence I had when originally developing Clojure. The journey continues!
(引退後は、もともとClojureを開発していたときのような自由と独立が戻ってきます。旅は続く!)

とのことで、今後のClojureの開発についても前向きな姿勢であることが窺えます。

Clojure/conj復活

コロナ禍の影響で2020年から開催が中止されていたClojure/conjが今年4年ぶりに開催されました。

Clojure/conj は世界最大級のClojureカンファレンスで、今年は2トラック25講演がありました。また、今回は初の試みとして配信チケットが販売され、リアルタイムでのオンライン配信が実施されました。

講演の内容はYouTubeで視聴可能になっています。

参考
https://clojure.org/news/2023/01/06/deref

Babashka Conf開催

今年は初めてBabashka単独のカンファレンスも開かれました。

Babashka はMichiel Borkent(@borkdude)が開発する非公式のClojureインタプリタで、GraalVMでビルドされ起動が速いため、Clojureライクな言語でスクリプトを書く用途で近年人気を集めています。当日は7講演と5つのLTがありました。

こちらも講演内容はYouTubeで視聴可能です。

参考

Clojure CLIがパッケージ管理ツールのシェア首位に

今年行われたClojureサーベイで、Clojure CLIが初めてClojureのパッケージ管理ツールとしてLeiningenを抜いて首位になったことが分かりました。

Clojure CLIは2017年に登場して以降年々シェアを拡大し、これまでのデファクトスタンダードだったLeiningenに迫っていましたが、今年になってLeiningenユーザの割合が60%超に対してClojure CLIユーザの割合が70%超と大勢が逆転しました。

これは、Clojure CLIではGitリポジトリやローカルディレクトリを直接依存ライブラリとして使える柔軟性があることや、Babashka等の外部ツールと依存ライブラリの定義を共有がしやすいこと、またtools.buildの登場によりClojure CLIでもビルドが容易になったこと等の複合的な理由によるものではないかと考えられます。

参考

REBLの後継ツール、Morseリリース

ClojureのインスペクタであるREBLの後継ツールとして、Morseが新たにリリースされました。

Morseは、前身のREBLと同じくREPLから使えるグラフィカルインスペクタで、操作性についてはおおむねREBLを踏襲しています。

REBLとの大きな違いは、MorseではReplicantという仕組みによって、別プロセス上にあるリモートオブジェクトを辿る際に、オブジェクトの一部のみを透過的にフェッチできることにより、巨大なマップや遅延シーケンスを効率的にトラバースできる点にあります。Replicantのサーバおよびクライアントはこのような機能を提供する独立したライブラリになっており、MorseはこのReplicant機能のデモンストレーション的な立ち位置になっています。

参考

Datomicが無償化

今年のClojure/conj開催に合わせて、Datomicが無償化されるアナウンスがありました。

Datomic はCognitectが開発する分散データベースで、一度追加したデータを変更できないデータモデルやDatalogというクエリ言語等の特徴を持ちます。Datomicは本格的な開発用途に対しては有料ライセンスで提供されていましたが、現在は無償で使えるようになっています(なお、オープンソース化はされていません)。

さらに6月には、クラウド(AWS)上で動くDatomic Cloudも後に続いて無償化が発表されました:

今後ますますDatomicの活用事例が増えていくかもしれません。

おわりに

しばらく落ち着き気味だったClojure界隈ですが、今年発表された機能改善は久しぶりにコンパイラに手の入るようないくつもあり、また待望されていたadd-lib機能が入ったので、それらが今後の言語の使用感や開発者体験にどれだけ大きな影響を与えるのか、期待して待ちましょう!

  1. そういう機能を提供する Pomegranate 等のサードバーティのライブラリはありましたが、公式にはそういった機能はありませんでした

16
2
0

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
16
2