Clojure Language Update 2015 〜一年の出来事と1.8の機能紹介〜


この記事は、Clojure Advent Calendar 2015 19日目のエントリーです。

先日、Clojure 1.8 RC4が公開され、1.8のリリースも間近に迫ってきています。今回はそんなClojure 1.8の機能紹介をメインに据えつつ、今年1年でClojure/ClojureScript界隈であったことを振り返ります。

今年1年であったこと

Clojure 1.7リリース

今年の前半にはまずClojure 1.7がリリースされました。かなり前からアナウンスされていたトランスデューサ(transducer)が導入され、シーケンスだけに限らない値の列に対する変換処理が統一的に書けるようになりました。また、reader conditionalが取り入れられ、それまではcljx等のツールを必要としていたClojureとClojureScriptの間でのコード共有が標準の範囲内でやりやすくなりました。
一方で、1.7のリリースにはわりと時間がかかり、例年のリリーススケジュールから大きくずれ込む結果になりました。そこで、1.8ではリリーススケジューリングをしっかりしようといくことになり[要出典]、Clojure/conjが行われる11月末頃を目処にリリースしようということになりました(が、残念ながらリリースにはもう少し掛かりそうです)。

ClojureScriptがセルフホスティングに

さて、ClojureScriptにとっても今年は大きなターニングポイントになりました。
まず、これまでClojureで書かれていたコンパイラがセルフホスティング、つまりClojureScript自身で書かれるようになりました。より正確には、Clojure 1.7で入ったreader conditionalにより、ClojureとしてもClojureScriptとしても解釈できるように書き換えられました。これによって、ClojureScriptがevalを持てるようになり、ClojureScript上にREPLを実現することが容易になりました。実際に、PlankRepleteなどのClojureScriptベースのREPLツールが出てきています。evalを持てるようになったことで、今後ClojureScript自身でマクロが書けるようになる展開もあるかもしれません。
また、Clojureとの開発の足並みを揃える目的で、従来のバージョニングから1.7.xというClojureに準じたバージョンが振られるようになりました。今後、ClojureとClojureScriptはより強固に連携をとって開発が進められていくでしょう。

ClojureコミュニティのSlackができた

Clojureコミュニティとして大きな動きとしては、Clojurianが集まるSlackチームが作られました(登録はこちらから)。現在、参加人数4000人、チャンネル数190弱と、非常に活発にやりとりがなされています。clojure/clojure-devやIRCなどの従来の方法でも依然としてやりとりされていますが、簡単なコミュニケーションをする場として便利な位置づけになっています。各国や地域別のチャンネルも開かれていて、日本語圏のClojureコミュニティ向けのチャンネル #clojure-japan もあります。

Clojureカンファレンス/ワークショップ

Clojureのイベントに関しては、最大規模のカンファレンスであるClojure/conjが4年目を迎え、はじめて2トラックでの開催になるほど年を追うごとに規模が大きくなってきています。また、ヨーロッパ最大のカンファレンスであるEuroClojureがCognitect主催に切り替わり、Clojure/conjとClojure Westに次いで3つめのClojure公式カンファレンスになりました。
昨年から始まった女性向けのワークショップを各地で開催しているClojure Bridgeは、今年も引き続き、アメリカをはじめとして、イギリスやドイツ、フィンランド、スウェーデンなどの多数の国で開催されました。
ワークショップつながりでいうと、去る12/12に日本でも初心者向けのワークショップ Clojureワークショップ(仮) が開催されました。参加者40人、TA10人を集めての規模のワークショップは日本では初めてではないでしょうか。今後も引き続き開催される予定とのことなので、Clojureを新たに学びたいという人にとっていい環境になってくれることを期待しています。

今年出版された本

今年も多くのClojure関連の本が出版されました(ソース)。

言語の入門的な内容の本だけでなく、つっこんだ内容についてClojureを使っているものも出てきているというのが興味深いですね。日本語の書籍で目立ったものがないのが若干残念な状況ではあります。

Clojure 1.8の新機能

以降では、Clojure 1.8について見ていきましょう。1.8でも大小さまざまな機能改善やバグフィックスが取り込まれています。そのなかでも、予定されている大きな変更はダイレクトリンクとソケットサーバの2つです。ここでは、この2つについて紹介します。

ダイレクトリンク

ダイレクトリンク(direct linking) は関数呼び出しのオーバーヘッドを小さくするための改善です。普通に使ってる分には特に意識することはないですが、一般的なClojureコードはほとんどいたるところ関数呼び出しで構成されているため、関数呼び出しの効率が改善されるのには大きなインパクトがあります。
ダイレクトリンクが具体的にどんなものか、サンプルコードを例に見てみましょう。たとえば、次のような2つの関数を定義する名前空間foo.coreを考えます:

core.clj
(ns foo.core)

(defn f [] 42)

(defn g [] (f))

これまでの関数呼び出しはVar経由でした。つまり、defnで作られるVarに束縛されている関数を、関数呼び出しのたびにderefしてinvokeしていました。

先ほどの例の場合では、関数gが呼ばれると、その実体であるfoo.core$gクラスのinvokeメソッドが呼び出されます。invokeメソッドの中ではまず、foo.core$gクラスがフィールドとして持っているVar #'foo.core/f を参照し、そのVarに束縛されている値(関数)をderefします。derefした結果、得られる関数はfの実体であるfoo.core$fクラスのオブジェクトなので、そのinvokeメソッドを呼び出します。この一連の流れによってgからのfの呼び出しが実現されています。

このようなVarを介した関数呼び出しの方式は、Clojureの動的性質の担保するために必要な仕組みでした。この仕組みによって、REPLやファイルのリロードによって実行時に関数定義を置き換えるようなことが実現されていました。しかし一方で、Varへの参照が途中に差し挟まることで、メモリアクセスが増えるばかりでなく、JITコンパイルによるインライン化も効きにくくなるため、効率面ではかなりの障害になっていました。

ダイレクトリンク方式では、関数呼び出しはもはやVarを介しません。関数オブジェクトのメソッドを直接呼び出すので、上に書いたようなオーバーヘッドもありません。

ダイレクトリンク方式でコンパイルされた場合、それぞれの関数オブジェクトにinvokeに加えてinvokeStaticというメソッドが追加されます。そして静的に関数が解決できる場合には、そのメソッドに対応する関数オブジェクトのinvokeStaticメソッドが呼び出されます。従来のinvokeメソッドは関数を高階関数の引数に渡した場合等、呼び出す関数が静的に特定できない場合のために残されています。

以上のように、Varを介さずに直接メソッドを呼び出すようになることで関数呼び出しのオーバーヘッドが取り除かれています。

ダイレクトリンクの効果は関数呼び出しの実行効率の改善だけにとどまりません。今まで関数オブジェクトごとに作られていた、Varを持っておくためのフィールドやコンスタントプール内の関連する値が不要になったため、メモリフットプリントも小さくすることができます。さらに、これまで関数オブジェクトのクラスがロードされる際に行なわれていた、Varをフィールドにセットする処理もいらなくなるため、起動時間も速くなるとされています。1.8からClojureのコアライブラリはダイレクトリンク方式でコンパイルされた状態で提供されるようになりますが、実際にclojure.jarのサイズは8%小さくなり、起動時間も10〜15%改善されたとの報告があります

一方で、ダイレクトリンクが導入されたことによって、実行時の定義の置き換えがうまくできなくなるので注意が必要です。たとえば、先ほどの名前空間foo.coreの例について、REPL上で関数fを再定義してみましょう。

user=> (require 'foo.core)
nil
user=> (in-ns 'foo.core)
#object[clojure.lang.Namespace 0x67582a96 "foo.core"]
foo.core=> (f)
42
foo.core=> (g)
42
foo.core=> (defn f [] 402)
#'foo.core/f
foo.core=> (f)
402
foo.core=> (g)
42
foo.core=> 

本来であれば、fを再定義すれば(g)の呼び出しでもそのfの変更が反映されているはずですが、ダイレクトリンクされていると(g)の呼び出しは(fの変更後も)元のfの定義を見に行ってしまうため変更が反映されません。

このように、ダイレクトリンクは効率的には大きなアドバンテージがあるものの、REPLを使ったインタラクティブ/インクリメンタルな開発とは相性が悪いのです。そのため、開発時はダイレクトリンクを無効にしておいて、プロダクションビルドする場合のみ有効にするといった運用が必要になってくるでしょう。

ダイレクトリンクはデフォルトでは無効になっています。ダイレクトリンクを有効にするには、システムプロパティにclojure.compiler.direct-linking=trueを追加します。javaコマンドから直接起動する場合は、起動オプションに -Dclojure.compiler.direct-linking=trueを指定します。Leiningenの場合は:jvm-optsに上のオプションを指定します

ソケットサーバ

最後にソケットサーバについてです。ソケットサーバ は簡易的なTCPのサーバを起動できるようにする仕組みです。主なユースケースとしては、コードをまったく書き換えることなくリモートのREPL機能を組み込むような場合が想定されています。仕組みとしてはREPLのみに限らず、標準入出力に対して読み書きする関数を書けば、サーバとして動作させることができる汎用的な仕組みになっています。標準入出力がソケットにつながっているという意味で、イメージ的にはCGIやinetdに似た仕組みと考えられるでしょうか。
ソケットサーバを起動するには、システムプロパティとして以下のような設定をします:

  • プロパティの名前は clojure.server.<server name> にする。<server name>は任意に決められる。
  • プロパティの値として以下の形式のEDN(マップ)を指定する。
    • :port - サーバが待ち受けるポート番号(整数)
    • :accept - ソケットの接続が確立するたびに呼ばれる関数の名前(名前空間修飾されたシンボル)

この他にもいくつかのオプションが指定できます(詳しくはドキュメントを参照)。
また、組込みのaccept用関数として、REPLサーバの実装である clojure.core.server/replが新たに用意されました。この関数を:acceptに指定することで、指定したポートで待ち受けるREPLサーバを組み込むことができます。
プロパティclojure.server.<server name><server name>をそれぞれ重複しないようにつければサーバを複数起動することもできます。Clojureの起動時にすべてのソケットサーバが立ち上げられます。

たとえば、次のようにしてechoサーバを作ることができます。こんな感じのコードを用意します:

core.clj
(ns echo-server.core)

(defn echo []
  (letfn [(prompt []
            (print "=> ")
            (flush)
            (read-line))]
    (loop [line (prompt)]
      (when line
        (println line)
        (recur (prompt))))))

そして、以下をproject.cljに追加します(Leiningenで立ち上げることを想定):

:jvm-opts ["-Dclojure.server.echo={:port 5555 :accept echo-server.core/echo}"]

適当なLeiningenタスクでClojureを起動しておいて、

$ lein repl                                                                                                 nREPL server started on port 51645 on host 127.0.0.1 - nrepl://127.0.0.1:51645
REPL-y 0.3.7, nREPL 0.2.10
Clojure 1.8.0-RC4
Java HotSpot(TM) 64-Bit Server VM 1.8.0_45-b14
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

user=> 
$

:portに指定したポートに接続しに行くと、echoサーバが起動されていることを確認できます。

$ telnet 127.0.0.1 5555
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
=> hoge
hoge
=> fuga
fuga
=> 

Leiningenから起動する場合だと、すでにnREPLが立ち上がっている状態なのであまり旨味が分かりにくいですが、プロダクションビルドしたJARだけがある状態でリモートREPLを起動したい、といった場合には効果を発揮するかもしれません。

まとめ

駆け足になりましたが、以上でひと通り今年一年の出来事と1.8の機能をおさらいしました。
今年もClojureは話題のつきない一年でした。日本でもチラホラと開発言語としてClojureを採用する事例を聞くようになってきました。これからもClojure/ClojureScriptがさらに盛り上がっていくことに期待していきましょう!!