読者です 読者をやめる 読者になる 読者になる

Clojure(ある意味)基礎文法最速マスター

(このエントリはLisp Advent Calendar 2012 1日目の記事です。)

はじめに

2年ほど前に、「◯◯言語基礎文法最速マスター」という、それぞれの言語の基本的な文法を説明したブログ記事を書くのが大流行した時期がありました。その流行り具合は、まとめ記事にまとめられた記事の数を見てもうかがい知ることができると思います(たとえば、こことか→プログラミング基礎文法最速マスターまとめ - ネットサービス研究室)。
それで、これだけたくさんの言語がカバーされてれば当然Clojureもあるだろうと思ってググってみましたが、意外にも2年経った今も「Clojure基礎文法最速マスター」はないみたいです。で、僕は入門記事みたいなのは書くのが苦手なのでやらないんですが、「Clojureの基礎文法とは何ぞや」ということを改めて考えてみました。
で、「基礎」をfoundationと考えれば、コンパイラによって直接解釈され、言語の核をなす「特殊形式」がある意味ではClojureの「基礎文法」といえるんでは?という考えに至りました。まぁ、そんなわけでこのエントリでは、Clojureの「基礎文法」としてClojureのすべての特殊形式を紹介したいと思います。Clojureの特殊形式とひとくちにいっても、Clojureを使ってる人なら誰でも知ってるようなものから、マクロの裏側でひっそりと使われていて普通のユーザはまずお目にかからないものまでいろいろあります。あなたはそのうちのいくつを知っているでしょう?

それでは、順にClojureの特殊形式を見ていきましょう*1

Clojureの特殊形式

def/if/do/quote/set!/try/catch/finally/throw

皆さんおなじみの特殊形式たち。特に説明はいらないでしょう。 (cf. Clojure - special_forms)

var

オブジェクトとしてVarを参照するときの特殊形式。あまり使いどころはないと思いますが、たとえばある名前空間の中でプライベートに定義されたVarを参照したい場合に使えます。

user=> (ns foo)
nil
foo=> (defn- f [x] (* x x))    ;; 名前空間fooでfという関数をプライベートに定義
#'foo/f
foo=> (in-ns 'user)
#<Namespace user>
user=> (f 4)                   ;; 普通には名前空間fooの外からfを参照できない
CompilerException java.lang.RuntimeException: Unable to resolve symbol: f in this context, compiling:(NO_SOURCE_PATH:9) 
user=> ((var foo/f) 4)         ;; 特殊形式varをVarオブジェクト経由でなら参照できる
16
user=> (#'foo/f 4)             ;; #'symbolの形式でも同様に参照できる
16
user=>

あとは、Varにつけられたメタデータを参照したい場合にも使えます。

user=> (meta #'cons)
{:ns #<Namespace clojure.core>, :name cons, :arglists ([x seq]), :added "1.0", :static true, :doc "Returns a new seq where x is the first element and seq is\n    the rest.", :line 22, :file "clojure/core.clj"}
user=>

実際に使ったことはないですが、なんらかのメタプログラミングなんかで使えるかもしれませんね。

monitor-enter/monitor-exit

JVMのモニターの機能を使う場合に使う特殊形式。陽に使うことはまずないですが、lockingマクロの実装なんかに使われています。

user=> (pprint (macroexpand '(locking o (Thread/sleep 10000) (println "done"))))
(let*
 [lockee__3952__auto__ o]
 (try
  (monitor-enter lockee__3952__auto__)
  (Thread/sleep 10000)
  (println "done")
  (finally (monitor-exit lockee__3952__auto__))))
nil
user=>
new

Javaクラスのコンストラクタを呼ぶ特殊形式。(new ...)は(. ...)と等価。

user=> (new String "foo") 
"foo"
user=> (String. "foo")
"foo"
user=> 

ちなみに、上の変換はマクロ展開時に行われます。

user=> (macroexpand '(String. "foo"))
(new String "foo")
user=> 
.(ドット)

オブジェクトのメンバ(フィールド/メソッド)にアクセスする特殊形式。(. )は(. )と等価。

user=> (. 1 toString)
"1"
user=> (.toString 1)
"1"
user=> 

ちなみに、上の変換もマクロ展開時に行われます。

user=> (macroexpand '(.toString 1))
(. 1 toString)
user=> 
let*

ローカル変数を束縛する特殊形式。letではなくlet*。letマクロの内部で使われています。Clojureでは、コンパイラなどが内部的に使うものの末尾に"*"をつける命名規則というか慣習があります。他のLisp方言にもlet*はありますが、"*"の意味合いは違っています。

マクロのletと特殊形式のlet*の違いは、letが分配束縛できるのに対して、let*は分配束縛に対応していない点です。逆にいうと、let*に対してマクロレベルで分配束縛を実現したのがletマクロといえます。

user=> (let [[x y] [1 2]] {:x x :y y})
{:x 1, :y 2}
user=> (let* [[x y] [1 2]] {:x x :y y})
CompilerException java.lang.IllegalArgumentException: Bad binding form, expected symbol, got: [x y], compiling:(NO_SOURCE_PATH:37) 
user=> 
fn*

let*と同様、fnマクロの内部で使われている特殊形式。fnマクロとの違いもほぼ分配束縛があるかないかという点のみです。

loop*/recur

同じくloopマクロの内部で使われている特殊形式。loop*/recurは、JVMでのループにコンパイルされるので最も高速なループ構文であるとともに、最も低レベルのループ構文でもあります。

letfn*

letfnマクロの内部で使われている特殊形式。letfnマクロとは第一引数の形式が若干異なります。

user=>
(letfn [(even? [x]
          (or (= x 0) (odd? (dec x))))
        (odd? [x]
          (and (not= x 0) (even? (dec x))))]
  (even? 42))
true
user=>
(letfn* [even? (fn [x] (or (= x 0) (odd? (dec x))))
         odd? (fn [x] (and (not= x 0) (even? (dec x))))]
  (even? 42))
true
user=> 

残念ながら、束縛される値をつくる式はfn/fn*のみに限られているようなので、これを悪用(?)して循環構造を作ったりはできないみたいです。

case*

caseマクロの内部で使われている特殊形式。case*の形式はcaseマクロに比べて非常に複雑です。

user=> (pprint (macroexpand '(case x 0 'zero 1 'one 'more)))
(let*
 [G__193 x]
 (case* G__193 0 0 'more {0 [0 'zero], 1 [1 'one]} :compact :int))
nil
user=>

細かい話は割愛しますが(よく分かってないので)、case*は各ケースの条件に指定されたリテラル値の型や値の並び方(連続した値かとびとびの値か)によって、JVMのtableswitch命令かlookuptable命令のどちらかにコンパイルされます。case*の最後の2引数はその分類を表しています。

reify*/deftype*

それぞれreifyマクロとdeftypeマクロの内部で使われている特殊形式。reifyマクロとreify*の違いは、reifyは以下の形式であるのに対して、

(reify 
  Foo
  (foo [this] "foo")
  Bar
  (bar [this] "bar"))

reify*は以下の形式であることです。

(reify* [Foo Bar]
  (foo [this] "foo")
  (bar [this] "bar"))

なぜこのような違いがあるのか詳しくは知りませんが、reify*の形式はJavaのクラスの構造(≒JVMの.classファイルの構造)に近く、より実装に都合がいい順序であるのに対して、reifyの形式はより可読性の高くなるように考えられた順序なのかもしれません。

同様の違いがdeftypeマクロとdeftype*の間にもあります。

import*

importマクロの内部で使われている特殊形式。importマクロではインポートするクラスを複数指定できたり、インポートするクラスをシンボルで指定するのに対して、import*では単一のクラスしか指定できなかったり、インポートするクラスを文字列で指定するなどの細々とした違いがあります。

user=> (pprint (macroexpand '(import 'java.io.Writer 'java.io.Reader)))
(do
 (clojure.core/import* "java.io.Writer")
 (clojure.core/import* "java.io.Reader"))
nil
user=> 

○○は特殊形式じゃないの?

defmacro

Clojureでは、defmacroは以下のようにdefnを使って実装されています。

user=> (macroexpand '(defmacro m [x] x))
(do (clojure.core/defn m ([&form &env x] x)) (. (var m) (setMacro)) (var m))
user=>
unquote/unquote-splicing

Clojureでは、unquoteやunquote-splicingがリード時にsyntax-quoteによって処理されるので、それらがコンパイラまで回ってくることはありません。ですので、特殊形式としては定義されていません。

おわりに

いかがだったでしょうか?知ってる特殊形式はいくつありましたか?
今回この記事で紹介した知識はほとんどの人にとってこの先の人生で役に立たないトリビアと成り果てるかもしれませんが、Clojureのコンパイラを移植したり、はたまたコードウォークを必要とする複雑なマクロを書くときには必要になるかもしれない知識でもあります。

次回(12/8)のエントリでは、そんなコードウォークを必要とするあるマクロの紹介をします。

*1:ちなみに、この記事はある程度基礎的なClojureの知識を持っている人を前提として書かれています。この記事でClojureに入門することはお薦めしません。