今年もアドベントカレンダーの季節がやってきました。この1年のClojure/ClojureScript界隈の動きを振り返ってみましょう。
Clojure 1.11
Clojureは次期バージョン1.11のリリースに向けて開発が進められていて、現在1.11.0-alpha3までがリリースされています。alpha3までに新たに追加された主な機能は以下のとおりです:
- キーワード引数をとる関数にマップが渡せるように
-
update-keys
/update-vals
:as-alias
- Java APIのラッパー関数強化
キーワード引数をとる関数にマップが渡せるように
Clojureには、分割束縛を使って関数にキーワード引数を渡せるようにするイディオムがあります:
(defn f [& {:keys [a b]}]
(- a b))
(f :a 5 :b 3)
;=> 2
(f :b 2 :a 10)
;=> 8
これは、関数に直接マップを渡すことでも似たことを実現できますが、キーワード引数として受け渡せるようにしておくことで括弧の分だけタイプ数が減って便利です。
一方で、このようなキーワード引数を下請け関数にそのまま渡すような機能はないため、キーワード引数をとる関数間で引数を取り回すのは若干面倒です。たとえば、以下の例のように、f
をキーワード引数を受け取る関数としたうえで、g
も同じキーワード引数を受け取り、かつg
からf
を呼び出す、というような場合、一旦g
でマップにまとめたキーワード引数をシーケンスに直してからapply
する必要があります:
(defn f [& {:keys [a b]}]
(- a b))
(defn g [& {:as opts}]
(apply f (apply concat opts)))
1.11では、こういったキーワード引数を受けとる関数にマップも直接渡せるようになりました:
(defn f [& {:keys [a b]}]
(- a b))
(defn g [& {:as opts}]
(f opts))
;; f, gともにマップもキーワード引数もとれる
(f :a 3 :b 2)
(f {:a 3, :b 2})
(g :a 3 :b 2)
(g {:a 3, :b 2})
ただし逆は不可で、マップ1つを受けとる関数にキーワード引数を渡せるようになるわけではないので注意が必要です。
参考: https://clojure.atlassian.net/browse/CLJ-2603
update-keys
/ update-vals
Clojure 1.11では update-keys
および update-vals
という2つの関数が新しく追加されました。
これらは元々 map-keys
, map-vals
として提案されていたもので、マップのそれぞれのキーもしくは値に与えられた関数を適用する関数です。
(update-keys m f)
および(update-vals m f)
はこれまでイディオムとしては以下のように書かれていたものです:
;; (update-keys m f)に相当するイディオム的な書き方
(->> m
(map (fn [[k v]] [(f k) v]))
(into {}))
;; (update-vals m f)に相当するイディオム的な書き方
(->> m
(map (fn [[k v]] [k (f v)]))
(into {}))
update-keys
, update-vals
はそれぞれ以下のように使えます:
(update-keys {"a" 0, "b" 1} keyword)
;=> {:a 0, :b 1}
(update-vals {:a 0, :b 1} inc))
;=> {:a 1, :b 2}
参考: https://clojure.atlassian.net/browse/CLJ-2651
:alias-as
また、ns
フォームの:require
節に対して、:alias-as
という新しいオプションが追加されました。
:alias-as
は、既存の:as
オプションと同様に、require
する名前空間に対してエイリアス(別名)をつけます。:as
との違いは、:alias-as
でエイリアスをつける対象となる名前空間が実際に存在しなくてもいい点です。
一見どう役立つのか分かりにくい機能ですが、clojure.specを使っている場合に特に有用性を発揮します。
clojure.specでは名前空間修飾されたキーワードを多用しますが、そこで使われる名前空間は実際に存在する名前空間である必要はなく、関連する複数のスペックをまとめるために自由に使うことができます。
しかし、こうして使われる名前空間名はユニーク性を保証するために比較的長くなりやすく、人手で直接入力するのが煩雑になりがちです。この問題に対するこれまでのワークアラウンドとしては、create-ns
関数とalias
関数を組み合わせて使うことで対応する名前空間を動的に作るような方法が採られていました。
:alias-as
はこのような使われ方する実在しない名前空間に対してエイリアスをつけられるため、名前空間名をすべて記述する必要性がなくなります。
:alias-as
の細かい仕様については現在まだ調整段階で完全に固まってはいませんが、現状では:alias-as
された名前空間が暗黙的にcreate-ns
で作られるようなセマンティクスになっています。
参考: https://clojure.atlassian.net/browse/CLJ-2123
Java APIのラッパー関数強化
さらに、Clojure 1.11では以下のようなJava APIのラッパー関数の追加が決定されています:
-
clojure.java.math
(java.lang.Math
に定義されたメソッド群のラッパー) -
Integer/parseInt
等のパース系メソッドのラッパー関数 -
Double/isNaN
およびDouble/isInfinite
のラッパー関数
これらにより、これまで(Long/parseLong s)
と書く必要があったのが (parse-long s)
と書けるようになり、(Double/isNaN d)
が(NaN? d)
と書けるようになります。
これらによってできることが大きく変わるわけではないですが、ファーストクラスの関数のため高階関数の引数に直接渡すことができることや、初心者にとってはJava APIより見つけやすさが向上することなどの利点が想定されています。
参考
- https://clojure.atlassian.net/browse/CLJ-2664
- https://clojure.atlassian.net/browse/CLJ-2667
- https://clojure.atlassian.net/browse/CLJ-2668
Clojure CLIのアップデート
Clojureの言語そのものに対するアップデートだけでなく、Clojure CLIへのアップデートも活発に行なわれました。
ビルド機能の強化
Clojure CLIへのアップデートで最も注目すべき機能改善は、プロジェクトビルドのサポートでしょう。
これまで、Clojure CLIはあくまで依存性を解決し、適当なクラスパスを設定したランタイムを起動するためのプログラムであって、Leiningenでいうところのlein compile
やlein uberjar
等のビルド機能は対象外という位置付けでした。
以下のような一連の機能が追加されたことにより、Clojure CLIを通じたプロジェクトのビルドがやりやすくなってきました:
-
-T
オプションの追加 tools.build
-
:deps/prep-lib
キーの追加
Clojure CLIによるビルドをするには、以下のようなスクリプトを用意します(実行にはClojure CLI 1.10.3.933以降を使う必要があります):
(ns build
(:require [clojure.tools.build.api :as b]))
(def lib 'my/lib1)
(def version (format "1.2.%s" (b/git-count-revs nil)))
(def class-dir "target/classes")
(def basis (b/create-basis {:project "deps.edn"}))
(def uber-file (format "target/%s-%s-standalone.jar" (name lib) version))
(defn clean [_]
(b/delete {:path "target"}))
(defn uberjar [_]
(clean nil)
(b/copy-dir {:src-dirs ["src" "resources"]
:target-dir class-dir})
(b/compile-clj {:basis basis
:src-dirs ["src"]
:class-dir class-dir})
(b/uber {:class-dir class-dir
:uber-file uber-file
:basis basis
:main my.lib.main}))
ここでは、clean
とuberjar
の2つのビルドタスクを定義しています。各ビルドタスクはClojureの関数として定義します。なお、ビルドタスクを書くのにtools.build
のAPIを使っています。tools.build
はプロジェクトのビルドで共通して必要になる機能をまとめたライブラリです。
deps.edn
で以下のように設定しておくと、
{...
:aliases
{:build {:deps {io.github.clojure/tools.build {:tag "TAG" :sha "SHA"}}
:ns-default build}}
...}
これらのビルドタスクは次のように-T
オプションで呼び出すことができます:
$ clj -T:build clean
$ clj -T:build uberjar
-T
オプションで起動されるコードは「CLIツール」と呼ばれます。-T
オプションの挙動は既存の-X
オプションとかなり似ています。-X
オプションとは異なり、-T
オプションはdeps.edn
で指定された:paths
や:deps
には影響されない(つまり、プロジェクトのクラスパスに捉われない)ため、プロジェクト自身のファイル等を処理するプログラムを実行するのに適しています。
参考
tools.tools
-T
オプションで起動できる「CLIツール」はプロジェクト内のコードだけではありません。Clojure CLI 1.10.3.933以降では、clojure -Ttools install
で環境にCLIツールをインストールすることができます。
たとえば、プロジェクトのscaffoldingをするためのCLIツールであるdeps-new
は次のようにインストールできます:
$ clojure -Ttools install io.github.seancorfield/deps-new '{:git/tag "v0.4.2"}' :as new
インストール時に:as <name>
としてCLIツールに名前がつけられ、clojure -T<name>
とすることでインストールしたCLIツールを起動することができます。
たとえば、deps-new
を上のように:as new
と名前をつけてインストールした場合、次のように呼び出して使うことができます:
$ clojure -Tnew app :name myusername/mynewapp
CLIツールのインストール以外にも、以下のようなCLIツールの管理機能がclojure -Ttools
を通じて提供されています:
-
clojure -Ttools list
: インストールされたCLIツールの一覧表示 -
clojure -Ttools show :tool <name>
: CLIツール<name>
の詳細情報表示 -
clojure -Ttools remove :tool <name>
: CLIツール<name>
のアンインストール
なお、clojure -Ttools
もtools.tools
というCLIツールとして実現されています、これはClojure CLIにバンドルされているCLIツールで、インストールせずに使うことができます。
各サブコマンドの使い方の詳細については、tools.tools
のAPIドキュメントを確認して下さい:
git 依存ライブラリがより扱いやすく
また、ちょっとした変更ですが、deps.edn
でgitリポジトリへの依存の指定が若干簡潔に書けるようになりました:
- 依存ライブラリの名前を
com.github.<user-name>/<repo-name>
等、あらかじめ決まった形式で指定すると:git/url
が省略可能になりました - 従来
:git/sha
にコミットハッシュをフルで指定する必要がありましたが、:git/tag
にタグ名を指定した場合には:git/sha
にコミットハッシュの(十分な長さの)プレフィックスを指定するだけでよくなりました
これらの改善により、deps.edn
にたとえば次のように書いていたものが、
{...
:deps {cognitect-labs/test-runner
{:git/url "https://github.com/cognitect-labs/test-runner.git"
:git/sha "48c3c67f98362ba1e20526db4eeb6996209c050a"}}
...}
以下のように書けるようになりました:
{...
:deps {io.github.cognitect-labs/test-runner
{:git/tag "v0.5.0" :git/sha "48c3c67"}}
...}
ClojureScriptのアップデート
ClojureScriptは、この1年間で4バージョン(1.10.844, 1.10.866, 1.10.879, 1.10.891)がリリースされました。ほとんどはGoogle Closure Compiler / Libraryのアップデートと不具合修正です。
そのなかで、ユーザレベルでも分かりやすい機能改善は、1.10.844で入った CLJS-3235 Library Property Namespace でしょう。これはdefault export
されたJavaScriptモジュールをClojureScriptから簡単に利用できるようにするための機能です。
これまで、default export
されたJavaScriptモジュールをClojureScriptから利用しようとすると、以下のようにする必要がありました:
(ns foo)
(def lib (.-default (js/require "npm-lib"))
1.10.844以降では、これを以下のようにns
フォームにまとめて記述できるようになりました:
(ns foo
(:require ["npm-lib$default" :as lib]))
この機能は、default export
だけでなく、モジュールからexportされている任意のプロパティを:require
するのにも使えます:
(ns foo
(:require ["npm-lib$subLib" :as sub-lib]))
参考
A History of Clojure 講演
HOPL IVが開催され、Rich Hickeyが話した "A History of Clojure" の講演動画が公開されました。
HOPL (History of Programming Languages)はプログラミング言語の歴史や発展についてをテーマとする不定期開催のカンファレンスです。当初は、昨年6月に第4回を開催予定でしたが、新型コロナの影響で開催が今年に延期されていました。
Clojure Deref開始
6月から、Clojureの開発コアチームが週ごとにまとめたClojure界隈のニュースが "Clojure Deref" として発行されるようになりました。
- Twitterアカウント: https://twitter.com/ClojureDeref
- RSSフィード: https://clojure.org/feed.xml
Clojure Derefは、コアチームがその週に取り組んでいたことのまとめをはじめ、その週にリリースされたClojureライブラリや公開されたClojure記事等が取り上げられます。
Clojure Deref自体は、コアチームの取り組みに関する透明性を求めるコミュニティ側の要望に応えて始まったもののようですが、それぞれの週にClojureコミュニティ全体でどういう出来事があったかをざっくり知るのに役立ちます。
Clojurists Together長期サポート枠開始
Clojurists Togetherでは今年から新たに1年間の長期の支援枠が開始されました。
Clojurists Togetherはこれまで、3ヶ月ごとに公募・選出した4つのOSSプロジェクトに各$3,000を出資し、支援する活動をしてきました。この定期の支援枠に加えて、今回年間$108,000の支援枠が追加され、よりインパクトの大きなOSSの開発に比較的長期に渡って取り組む開発者を支援することができるようになりました。
また、この枠は従来の支援枠とは異なり、プロジェクト単位の支援ではなく、開発者単位の支援であるため、1人で多くのプロダクトの開発を手掛ける影響力の大きな開発者が、自分の裁量によってプロダクトへのリソースの配分を自由に決定でき、より柔軟な枠組みといえるでしょう。
今回、この長期支援枠に選ばれたのは以下の6人です(括弧内は各開発者の主要なOSSプロダクト):
- Bozhidar Batsov (CIDER, nREPL)
- Michiel Borkent (clj-kondo, babashka)
- Dragan Djuric (Neanderthal)
- Thomas Heller (shadow-cljs)
- David Nolen (ClojureScript, Krell)
- Nikita Prokopov (DataScript, Rum)
参考: https://www.clojuriststogether.org/news/long-term-funding-selections/
Clojarsへのライブラリ登録に承認済みグループ名必須に
Clojureライブラリの配布に使われるリポジトリであるClojarsへのライブラリ登録に、今年4月から「承認済みグループ名」が必須となりました。
これは、PythonのPyPIやJavaScriptのnpm等のパッケージリポジトリで、Dependency confusion攻撃のように、特定のパッケージと同名の悪意のあるパッケージを配置することで意図しないコードを読み込ませる攻撃が横行したことへの対策の一環です。
これまで、Clojarsでライブラリを配布する際には、<groupId>/<artifactId>
という形式 (たとえば、ring/ring-core
など) でライブラリに名前をつける必要がありましたが、<groupId>
と<artifactId>
の組み合わせがユニークであるという以上の制約条件はありませんでした。
今回の変更により、<groupId>
が「承認済みグループ名 (verified group name)」である必要がある、という条件が追加されました。
「承認済みグループ名」は逆ドメイン名ベースのグループ名で、以下のいずれかである必要があります:
- Clojarsの管理者から個別で承認を受けたグループ名
- Clojarsによってあらかじめ用意されたグループ名
Clojarsの管理者から個別の承認を受けるには、clojars/administration から申請して、グループ名として使うドメインを自分が保持していることを証明する必要があります。
Clojarsによってあらかじめ用意されたグループ名は別途申請する必要がなく、Clojarsアカウント毎に以下のグループ名が使えるようになっています:
org.clojars.<ユーザ名>
net.clojars.<ユーザ名>
また、GitHubやGitLabをClojarsのアカウントと紐付けると、以下のグループ名も使うことができます:
-
com.github.<GitHubユーザ名>
/io.github.<GitHubユーザ名>
com.gitlab.<GitLabユーザ名>
この措置は今年4月以降に新しく登録されるすべてのライブラリが対象となり、すでにライブラリを登録しているユーザでも新しいライブラリを登録する際には承認済みグループ名が必要となります。ただし、4月以前にリリースされたライブラリの新しいバージョンをリリースする際はこれまで通りのグループ名を使うことができます。
参考
- https://github.com/clojars/clojars-web/wiki/Verified-Group-Names
- https://clojureverse.org/t/clojars-verified-group-names/7297/2
StackOverflow Developer SurveryにClojure復活
毎年StackOverflowが開発者を対象に行なっているサーベイ StackOverflow Developer Survey にClojureが復活しました。
Clojureは、例年のサーベイ結果では「もっとも好きな言語」「収入の高い言語」等のランキングで上位に常連で入っていますが、昨年のサーベイ結果では、ErlangやF#とともに特別な説明もなくランキングからは除外されていました。
今年のサーベイ結果では、他の言語とともに晴れて復活を果たし、「もっとも好きな言語」では第2位、「収入の高い言語」では第1位にランクインし、(一部界隈での)変わらぬ人気を示しました。