Clojure(ある意味)基礎文法最速マスター
(このエントリはLisp Advent Calendar 2012 1日目の記事です。)
はじめに
2年ほど前に、「◯◯言語基礎文法最速マスター」という、それぞれの言語の基本的な文法を説明したブログ記事を書くのが大流行した時期がありました。その流行り具合は、まとめ記事にまとめられた記事の数を見てもうかがい知ることができると思います(たとえば、こことか→プログラミング基礎文法最速マスターまとめ - ネットサービス研究室)。
それで、これだけたくさんの言語がカバーされてれば当然Clojureもあるだろうと思ってググってみましたが、意外にも2年経った今も「Clojure基礎文法最速マスター」はないみたいです。で、僕は入門記事みたいなのは書くのが苦手なのでやらないんですが、「Clojureの基礎文法とは何ぞや」ということを改めて考えてみました。
で、「基礎」をfoundationと考えれば、コンパイラによって直接解釈され、言語の核をなす「特殊形式」がある意味ではClojureの「基礎文法」といえるんでは?という考えに至りました。まぁ、そんなわけでこのエントリでは、Clojureの「基礎文法」としてClojureのすべての特殊形式を紹介したいと思います。Clojureの特殊形式とひとくちにいっても、Clojureを使ってる人なら誰でも知ってるようなものから、マクロの裏側でひっそりと使われていて普通のユーザはまずお目にかからないものまでいろいろあります。あなたはそのうちのいくつを知っているでしょう?
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に入門することはお薦めしません。
逆FizzBuzz問題をTrieでトライ
なにやら巷で逆FizzBuzz(Inverse FizzBuzz)問題というのが流行ってるらしい。
- 逆FizzBuzz問題 (Inverse FizzBuzz) - 猫とC#について書くmatarilloの雑記
- F#で逆FizzBuzz問題 - Bug Catharsis
- わぁいInverse Fizzbuzz解けたよー - 這い寄るゆろよろ・アンド・ライジングフォース日記
- Inverse Fizzbuzz - scalaとか・・・
Shipper: すみませんが問題をもう一度教えてもらえますか?
逆FizzBuzz問題 (Inverse FizzBuzz) - 猫とC#について書くmatarilloの雑記
Google: あるリストが与えられたときに、FizzBuzzを実行するとそのリストを出力するような最短の連続数列を求めよ。
「ここまで短く書けるよ!」という向きは他でもいろいろとやられているのでよそに譲るとして、ここではTrie(トライ)を使った方法でやってみた。
こんな考え方
Fizz, Buzz, FizzBuzzの出現の仕方には周期性があって、3つの並びとして許されるのは「その周期のうちのどこから始めるか」という7パターンのバリエーションしかない。
なので、その7パターンに対してTrieを作ってやって、並びを先頭から調べていけば、7つのパターンのうちのどのパターンかが判別できる。
あとは、並びを末尾まで調べて、許されない順序でFizz, Buzz, FizzBuzzが並んでいないかを調べればいい。任意の長さの並びを調べるためには、上の図で緑色で示したノードの先にも無限に伸びたTrieが必要になるので、今回は遅延評価で無限に伸ばせるようなTrieを作った。
コード
(ns inverse-fizzbuzz (:refer-clojure :exclude [range extend]) (:use [clojure.test :only [deftest are]])) (defprotocol Node (range [this]) (children [this])) (extend-type clojure.lang.PersistentVector Node (range [this] (first this)) (children [this] (second this))) (defn min-next [word n] (let [m ({:fizz 3, :buzz 5, :fizzbuzz 15} word)] (* m (+ 1 (quot n m))))) (defn extend [[word & words] start end] (reify Node (range [this] [start end]) (children [this] {word (extend words start (min-next word end))}))) (def trie (let [words (cycle [:fizz :buzz :fizz :fizz :buzz :fizz :fizzbuzz])] [nil {:fizz [[3 3] {:fizz (extend (nthrest words 4) 6 9) :buzz [[9 10] {:fizz [[3 6] {:fizz (extend (nthrest words 4) 3 9) :fizzbuzz (extend words 9 15)}]}] :fizzbuzz (extend words 12 15)}] :buzz [[5 5] {:fizz [[5 6] {:fizz (extend (nthrest words 4) 5 9) :fizzbuzz (extend words 10 15)}]}] :fizzbuzz (extend words 15 15)}])) (defn solve ([words] (solve words trie)) ([[word & words' :as words] trie] (if (empty? words) (range trie) (when-let [trie' ((children trie) word)] (recur words' trie'))))) (deftest test-solve (are [x y] (= (solve x) y) [:fizz] [3 3] [:fizz :buzz] [9 10] [:buzz :fizz :fizz] [5 9] [:fizz :fizz :buzz] [6 10] [:fizzbuzz :fizz] [15 18]))
おわりに
他と比べるとコードが長いのがアレだけど、Trieを使ってるので
- 入力のリストを1回舐めれば結果が出せる
- 変な順序の入力は即座に弾くことができる
あたりが利点になるかなぁと思う。
Clojureでsyntactic closureを使ってhygienic macroを書くためのライブラリを作りました
という話。当初は実現可能性を示すことを目的にしていましたが、それなりに実用できそうな感じになってきたのでライブラリとしてまとめました。
hygienic macroとは? syntactic closureとは?
詳細な説明はよそへ譲りますが、大雑把にいうと、hygienic macroとは識別子の衝突を自動的に回避する仕組みをもったマクロのことをいいます。hygienic macroについての研究は主にSchemeに対して行われていて、R5RSやR6RSに取り入れられているsyntax-rulesやsyntax-caseもhygienic macroの実装のひとつです。
syntactic closureはhygienic macroの別の実装で、hygienic macroの中ではもっとも単純なもののひとつです。
hygienic macroやsyntactic closureについての文献は少ないですが、それらについて書かれたウェブページとしてはこのあたりが参考になるかもしれません。
- syntactic-closures - The Chicken Scheme wiki
- Community-Scheme-Wiki: syntactic-closures
- Hygienic macro - Wikipedia, the free encyclopedia
hygienic macroとCommon Lispのマクロのようないわゆる「伝統的なマクロ」で表現力を比較すると、syntax-case等によってCommon Lispのdefmacroが定義できる一方、hygienic macroでは、レキシカルスコープを"曲げる"ような、伝統的なマクロでは不可能なことができるため、hygienic macroの方が伝統的なマクロより表現力が高いといえます。
Clojureのマクロシステム
Clojureのマクロシステムは伝統的なマクロをベースとしたもので、hygienic macroではありません。しかし、syntax-quoteを使うことでほとんどの場合で識別子の衝突を引き起こさないマクロを書くことができます。
syntax-quoteは、Schemeのquasiquoteのようにマクロのテンプレートとして使うことができますが、それに加えて次のことをリード時に実行します。
- シャープ(#)で終わるシンボルをgensymで生成したシンボルに置き換える(auto-gensym)
- それ以外のシンボルを名前空間で修飾する
user=> `(let [x# 42] (* x# x#)) (clojure.core/let [x__490__auto__ 42] (clojure.core/* x__490__auto__ x__490__auto__)) user=>
これにより、実用的な範囲ではほぼhygienic macroと同じことが可能ですが、上で触れたようなレキシカルスコープを曲げるような芸当は(少なくとも素直には)できないので、Clojureのマクロシステムはhygienic macroより表現力が低いように見えます。このあたりの詳細についてはそのうち別エントリで書きたいと思います。
また、auto-gensymは便利ですが、限界もあって万能ではありません。以下の例を見ると分かるとおり、
user=> [`(x# x#) `(x# x#)] [(x__497__auto__ x__497__auto__) (x__498__auto__ x__498__auto__)] user=>
auto-gensymでは、シャープで終わるシンボルは同じ名前であっても別のsyntax-quoteに現れれば別のgensymに置き換えられます。したがって、たとえば
(defmacro my-doto [x & forms] `(let [obj# ~x] ~@(for [[method & args] forms] `(~method obj# ~@args)) obj#))
というようにマクロを定義すると、2行目と5行目のobj#と4行目のobj#は別々のシンボルに展開されてしまいます。
(macroexpand '(my-doto (java.util.HashMap.) (.put "a" 1) (.put "b" 2))) ;=> (let* [obj__535__auto__ (java.util.HashMap.)] (.put obj__534__auto__ "a" 1) (.put obj__534__auto__ "b" 2) obj__535__auto__)
これを回避するには従来通りgensymを使う必要があり不便です。
このライブラリは何をしているか
前置きがだいぶ長くなりました。
上で書いたように、Clojureのマクロシステムでは、束縛変数の衝突回避(=auto-gensym)に難があったりするものの、自動的に名前空間を修飾することによる自由変数の衝突回避はうまく機能していそうです。
そこでこのライブラリでは、自由変数の衝突回避にはClojureの従来のマクロシステムと同じ自動的な名前空間の修飾を使いつつ、束縛変数の衝突回避にはSyntactic Closuresの元論文*1に沿って束縛変数の自動的なリネームを取り入れ、hygienic macroを実現しています。このあたりの実装の詳細についても、なんだかいろいろと怪しげなマクロのテクニックを使ったので、気が向いたら別エントリで書きます。
使い方
ライブラリのインストールはLeiningenを使っていれば、次をproject.cljの:dependenciesに追加し、lein depsするだけでできます。
[syntactic-closure "0.1.0"]
ライブラリを使う場合には、
(use 'syntactic-closure.core)
とします。これで、以下のようなsyntactic closureによるhygienic macroが定義できるようになります。
(define-syntax let1 [name init & body] (sc-macro-transformer (fn [env] (quasiquote (let [~name ~(make-syntactic-closure env nil init)] ~@(map #(make-syntactic-closure env [name] %) body))))))
macroexpandしてみると、束縛変数が自動的にリネームされるのが分かります。
(macroexpand '(let1 x 10 (* x x))) ;=> (let* [x403 10] (clojure.core/* x403 x403))
また、syntactic-closure.coreで提供されているScheme由来の関数やマクロは、名前が長かったり書き方が冗長だったりするので、名前空間syntactic-closureではこれらの略記版を提供しています。
(use 'syntactic-closure) (defsyntax let1 [name init & body] (qq (let [~name ~^:? init] ~@^{:? name} body)))
まとめ
ということで、syntactic closureを使ってClojureでもhygienic macroを定義するライブラリを書きました。とはいえ、自分でもいまいち「hygienic macroとは何ぞや」というところがすべて理解できているわけではないので、間違い等ありましたらご指摘いただけると助かります。
ちなみに1年ほど前にClojureでsyntax-rules(とsyntax-case)を使えるようにするライブラリというのが話題になったことがありましたが、あちらはマクロの入力フォームをパターンマッチして分解することに主眼を置いていて、hygienic macroが定義できるわけではないようです。
真・自然言語プログラミングの"the"演算子をClojureで
みなさん、明けましておめでとうございます。今年もよろしくお願いします(遅
さて、今回のネタは"the"演算子をClojureで実装してみたという話。"the"演算子といってもCommon Lispのtheではありません。
2年半ほど前に、FLTV(Future Language TV)という未来の言語を妄想するイベントがありました。そこで@kinabaさんが「Rhetorical Programming 真・自然言語プログラミング」というタイトルで発表をされていました*1。その発表は、自然言語から(構文ではなく)機能を借りてくるという発想のもと、3つの強力な機能について紹介したものでした。そのうちのひとつが"the"という演算子で、「文脈から何を指しているのか特定できる」英語の"the"から機能を借りてきたものです。発表資料では「型を1つ引数にとり現在のスコープに唯一存在するその型のオブジェクトを返す演算子」と説明されています。Clojure的に書くと、
(let [^File _ (File. "foo.txt") ^FileReader __ (FileReader. (the File)) ^BufferedReader ___ (BufferedReader (the FileReader))] (.readLine (the BufferedReader)))
みたいなコードが動くようになるわけです。で、これの何が嬉しいかというと、変数の名前をいちいち考えなくていいよね、ということみたいです。実際には、上の例のように、_とか__といったような適当な一意な名前をつけてやる必要があって意外と面倒なんですが、これについては下のようなマクロを書いてやれば対処できます(letに関して、は)。
(defmacro anonymous-let [bindings & body] `(let ~(vec (mapcat #(list (with-meta (gensym) {:tag (:tag (meta %))}) %) bindings)) ~@body)) (anonymous-let [^File (File. "project.clj") ^FileReader (FileReader. (the File)) ^BufferedReader (BufferedReader. (the FileReader))] (.readLine (the BufferedReader)))
実装
それで、theマクロの実装ですが、これは実は非常に簡単に実現できます。
(defmacro the [t] (first (filter #(= (:tag (meta %)) t) (keys &env))))
キモはこのブログで何回か紹介しているマクロの暗黙の引数&envです。&envで受け取るローカル環境には、変数につけられたメタデータもついてくるので、theマクロの引数に与えられた型がタグとしてつけれられている変数を抜き出してくれば所望の挙動が得られます。&envすげー。
後記
なんやかんやと書きましたが、実際の着想は順序が逆で、&envとメタデータを使って何かおもしろいことができないかなぁと思っていたところに、ふと以前見た@kinabaさんの発表のことを思い出したという感じ。
僕がマクロの暗黙の引数&envと&formに関心を持っているのはなぜかというと、通常マクロではマクロの引数に与えた情報しか知ることができないのだけど、&envや&formを使うことでマクロ呼び出しの外側からマクロに情報を与えることができるからです。ただ、これを使うことでできることは確実に増えてるはずだけど、具体的にどういう場面で使うのが便利なのか未だよく分からず。今回の例はそれなりに実用できそうな応用例かなぁと思うけど、もっとおもしろい使い方を知ってる方がいればぜひ教えてもらいたいですね。
*1:ちなみに、発表資料はここからダウンロードできるみたいです。http://www.kmonos.net/pub/Presen/fltv/FLTV.pdf
重箱の隅的Clojureのイケてないところ
この記事はClojure Advent Calendar 2011 4日目の記事です。
Clojureには,シーケンスや組み込みのイミュータブルなコレクション型,STMなどエレガントだといわれる様々な特徴がある。一方で,ときにプログラマを悩ませるような厄介な挙動や不自由さを感じる制約も結構あったりする。
今回は,僕がClojureを使い始めてから2年ちょっとの間に遭遇した,そういったClojureの"イケてない"部分のうち,現在の最新バージョン(v1.3.0)でも見られるものについてまとめてみた。
ネストしたloopをうまく扱えない
ClojureはJVM上で実行されるため,(今のところ)末尾呼び出しが最適化されない。末尾再帰(ループ)を使いたい場合にはloop - recurを使う。たとえば,階乗を計算する関数factはloopを使って下のように定義できる。
(defn fact [n] (loop [n n, ret 1] (if (= n 0) ret (recur (- n 1) (* n ret)))))
関数の最初からループをする場合には,loopを使わずに単にrecurを呼べばいい。
(defn fact [n ret] (if (= n 0) ret (recur (- n 1) (* n ret))))
さて,末尾再帰が最適化されないのは残念だが,百歩譲ってそれを認めたとしても,recurというキーワードを導入したことは間違いだと個人的には思う。それは,ひとつにはloopをネストして使うことができないためだ。
(loop [m m] ... (loop [n n] ... (recur ...) ...) ...)
というコードを書くと,内側のループ内で使われているrecurでは内側のループの先頭まで戻ることはできても,外側のループの先頭まで戻ることはできない。これではラベル指定付きcontinue文をもつJavaよりも制御をうまくできない。
これを解決するClojure的なやり方は,おそらくtrampolineを使う方法だろう。
(trampoline (letfn [(loop1 [m] ... (letfn [(loop2 [n] ... #(loop1 ...) ...)] (loop2 n)) ...)] (loop1 m))
しかし,この方法にはいくつか問題がある。1つは,コードが読みにくくなること。もう1つは,効率が悪くなることだ。Clojureのloopは,Javaでループを書くのと同じくらい効率的なコードにコンパイルされる,Clojureのループ構文中ではもっとも効率のよいループ構文だ。trampolineを使うと,ループのたびにクロージャが生成されるためどうしても効率が落ちる。
この問題を効率を落とさずに解決する方法のひとつとして個人的に提案したいのは,Schemeのnamed letのようにloopごとに名前をつけられるようにし,
(loop recur1 [m m] ... (loop recur2 [n n] ... (recur1 ...) ...) ...)
と書けるようにloop構文を変更することだ。そのうえで,loopの名前を省略した場合のデフォルトの名前を"recur"にしておけば今のloopの完全な上位互換となり,効率を落とさずより柔軟な制御を記述できるようになると思う。
(let [x v] ...) != ((fn [x] ...) v)
これは,Clojureがrecurというキーワードを導入したことが間違いだと思うもう1つの理由だ。
なんかを読むと,Schemerはletとlambdaを自然に読み替えるといったことが書かれている。Clojureでも,状況はだいたい同じようなもので,基本的には
(let [x v] ...)
という式は
((fn [x] ...) v)
と等価だと考えられていると思う。
ところが,Clojureではこの2つの式は等価ではない。違いはrecurの扱いにある。上でも書いたとおり,関数中に現れるrecurは関数の最初からのループを表す。ところが,let中のrecurはletの外にあるloopや関数へのループとなる。
この違いは,特にマクロを定義するときに問題になる場合がある。たとえば,
(defmacro with-open [[name expr] & body] `(let [~name ~expr] (try ~@body (finally (.close ~name)))))
と定義していたマクロを
(defmacro with-open [[name expr] & body] `(letfn [(f# [x#] (try ~@body (finally (.close x#)))] (f# ~expr)))
と定義したとする。このとき,(やや人工的な例だが)
(loop [files ["hoge.txt" "fuga.txt"]] (if (empty? files) 'done (let [[file & files] files] (with-open [w (writer file)] (spit w "hihi") (recur files)))))
というようにwith-openの本体中でrecurを使うとエラーになる。recurが,with-open内部のローカル関数のループとして展開されてしまうためだ。これは,コードを見ても怪しげな部分がないにも関わらずエラーになるうえ,エラーメッセージを見ても何が原因なのか分からない(例の場合は,「clojure.lang.PersistentVector$ChunkedSeqクラスにcloseというフィールドはない」というエラーになる)ため,非常に厄介なバグになる可能性がある。
まとめ
以上で説明したのは,あくまで僕個人が遭遇したClojureのイケていない部分のうちの一部にすぎないが,特に「コードの見た目から予想される挙動と実際の挙動が異なる」というものが多い。コードの見た目と実際の挙動が異なるのは,見つけにくいバグを埋め込む原因になりやすい。それらの差異が,ぜひ今後小さくなっていくことを期待したい。
Shibuya.lisp TT #7でLT発表してきました
遅ればせながら,10/22(土)に行われたShibuya.lispの第7回テクニカルトークに参加した報告。いつの間にか1週間も過ぎていましたね(^^;
Shibuya.lispのテクニカルトークももう7回目ということですが,東京が遠いのとこれまでなかなか都合が合わなかったのとで念願(?)の初参加でした。で,せっかく参加するのに普通に発表を聞きに行くだけというのもアレだな,というわけでLT枠で発表させてもらいました。
ネタはClojureのマクロで,JVMのバイトコードが書けるインラインアセンブラを実装した話。実装も発表資料作りも結構ギリギリのスケジュールでやってたので発表もほぼぶっつけでしたが,「まぁどうにかなった」という感じでした。ネタ自体は,時間の都合と参加者全員がClojureを知っている人ばかりじゃないということから端折った部分もあるので,気が向いたらまた別エントリで書けたらと思ってます。
さて,参加してみての感想。Lispだけのイベントでよくもまぁこれだけの人が集まるなぁというのが第一印象。しかも,前回のテクニカルトークからわりと期間が開いていたいたためか発表者も多く(当初LTは6枠だったはずが最終的に9枠に),内容もゴツいのが多くありました。というか,処理系作りました系のネタが非常に多かったですね。人類は生命の危機を感じると子孫を残そうとする本能をもつとかいう話を聞いたことがありますが,2011年の危機的状況にあってLisperたちには処理系を残そうとする本能が働いたようですね。たぶん。
懇親会はTwitter上でしか面識のなかった人たちと話ができて楽しかったです。東京のイベントには今までほとんど参加したことがなかったので,こういう機会に人脈が広げられるのは嬉しい限りですね。
それから懇親会中,id:lequeさんによるcall/ccのレクチャーが繰り広げられていて,結構な数の人が説明を聞いていたのが印象的でした。Lisperで集まるイベントとはいえ,意外と皆さん自分がメインで使っている言語以外は知らないことも多いんじゃないでしょうか(僕もSchemeとClojure以外はほとんど分かりませんし)。なので,Shibuya.lispのテクニカルトークでも「SchemerのためのClojure入門」とか「多忙なCommon LisperのためのEmacs Lispガイド」とか,そういうネタをやってもタメになるんじゃないかと思いました。
まぁそんなわけで有意義なLispまみれの半日を過ごすことができました。そして,今回参加したことでまたLispで何か作るぞ熱が高まったので,なんか作れたらいいなぁと思います。
最後になりましたが,素晴らしいイベントを開催して下さった運営スタッフの皆さん,ありがとうございました&お疲れ様でした。次回のテクニカルトークも都合が合えばぜひ参加したいと思います。
Clojure 内部で使われる中間形式 Expr について分かったこと
最近,暇を見つけてClojureのコンパイラを読んでいる。まだよく分かっていない部分もたくさんあるけれど,今まで読んできた中で分かったこと,特にClojureの内部で使われている "Expr" という中間形式についてまとめておく。
Expr って何?
Clojureの内部では,以下のような処理が行われている。
Exprは,次のように定義されるインタフェースで,抽象構文木の各ノードを表す。
interface Expr{ Object eval() throws Exception; void emit(C context, ObjExpr objx, GeneratorAdapter gen); boolean hasJavaClass() throws Exception; Class getJavaClass() throws Exception; }
重要なのが eval と emit という2つのメソッド。大まかにいえば,REPL(インタプリタ)から呼ばれるのがevalで,コンパイラから呼ばれるのがemitだ。
Clojureの構文それぞれについてExprの実装クラスが用意されていて,全部でだいたい50個くらいある。
$ sed -n -e '/class [a-zA-Z]*Expr/p' Compiler.java public static abstract class UntypedExpr implements Expr{ static class DefExpr implements Expr{ public static class AssignExpr implements Expr{ public static class VarExpr implements Expr, AssignableExpr{ public static class TheVarExpr implements Expr{ public static class KeywordExpr extends LiteralExpr{ public static class ImportExpr implements Expr{ public static abstract class LiteralExpr implements Expr{ static public abstract class HostExpr implements Expr, MaybePrimitiveExpr{ static abstract class FieldExpr extends HostExpr{ static class InstanceFieldExpr extends FieldExpr implements AssignableExpr{ static class StaticFieldExpr extends FieldExpr implements AssignableExpr{ static abstract class MethodExpr extends HostExpr{ static class InstanceMethodExpr extends MethodExpr{ static class StaticMethodExpr extends MethodExpr{ static class UnresolvedVarExpr implements Expr{ static class NumberExpr extends LiteralExpr implements MaybePrimitiveExpr{ static class ConstantExpr extends LiteralExpr{ static class NilExpr extends LiteralExpr{ static class BooleanExpr extends LiteralExpr{ : : (以下略)
Expr を解析するツール
ClojureプログラムからExprへのパースは,clojure.lang.Compilerクラスのanalyzeメソッドを呼び出すことで実行される。Compilerクラスといえども一介のJavaクラスなので,ClojureのJava interopの機能を利用してClojureから呼び出すことができる。そこで,analyzeによるパースを適当にClojureのオブジェクトに変換してくれるツールを作った。ソースコードはanalyze.clj。
簡単な実行例はこんな感じ。
Clojure 1.2.0 user=> (use 'analyze) nil user=> (analyze 0) {:$ ConstantExpr, :v 0} user=> (analyze ''(0 1)) {:$ ConstantExpr, :v (0 1)} user=> (analyze [0 1]) {:$ VectorExpr, :args [{:$ ConstantExpr, :v 0} {:$ ConstantExpr, :v 1}]} user=> (analyze "hoge") {:$ StringExpr, :str "hoge"} user=> (analyze 'cons) {:$ VarExpr, :var #'clojure.core/cons} user=> (analyze '(+ 0 1)) {:$ StaticMethodExpr, :c clojure.lang.Numbers, :method-name "add", :args [{:$ ConstantExpr, :v 0} {:$ ConstantExpr, :v 1}]} user=> (pprint (analyze '(fn [] 0))) {:$ MetaExpr, :meta {:$ MapExpr, :keyvals [{:$ KeywordExpr, :k :line} {:$ ConstantExpr, :v 9}]}, :expr {:$ FnExpr, :methods [{:$ FnMethod, :req-parms [], :rest-parms nil, :body {:$ BodyExpr, :exprs [{:$ ConstantExpr, :v 0}]}}]}} nil user=>
analyzeの内部では,clojure.lang.Compilerクラスのanalyzeメソッドを呼び出し,結果として返ってきたExprオブジェクトを適当にClojureのMapに変換している。analyzeを2引数で呼び出すと,第1引数で解析するコンテキストを指定することができる。コンテキストには,EVAL, EXPRESSION, RETURN, STATEMENT の4つがある。コンテキストの違いについてはいまいちよく分かっていないけど,EVALがREPLから入力されたトップレベルのフォーム,EXPRESSIONが通常の式,RETURNが末尾位置,STATEMENTが値を捨てていいコンテキストを表しているらしい,という理解。
指定するコンテキストが異なると解析できない構文もある。このあたりも謎。
user=> (pprint (analyze EVAL '(let [x 0] x))) {:$ InvokeExpr, :fexpr {:$ FnExpr, :methods [{:$ FnMethod, :req-parms [], :rest-parms nil, :body {:$ BodyExpr, :exprs [{:$ LetExpr, :binding-inits [{:$ BindingInit, :binding {:$ LocalBinding, :sym x, :init {:$ ConstantExpr, :v 0}}}], :body {:$ BodyExpr, :exprs [{:$ LocalBindingExpr, :b {:$ LocalBinding, :sym x, :init {:$ ConstantExpr, :v 0}}}]}}]}}], :compiled-class user$fn__1056}, :args []} nil user=> (pprint (analyze EXPRESSION '(let [x 0] x))) java.lang.NullPointerException (NO_SOURCE_FILE:81) user=> (pprint (analyze RETURN '(let [x 0] x))) java.lang.NullPointerException (NO_SOURCE_FILE:82) user=> (pprint (analyze STATEMENT '(let [x 0] x))) java.lang.NullPointerException (NO_SOURCE_FILE:83) user=>
インタプリタとコンパイラをつなぐ ObjExpr
さっき「evalはインタプリタから呼ばれ,emitはコンパイラから呼ばれる」とざっくり説明したが,この2つは完全に分業しているわけじゃない。というのも,Clojureの関数はJavaのオブジェクトとして実現されていて,そのためにClojureのインタプリタでは,オンザフライでクラスを生成する必要があるためだ。他に,reify や deftype の実現にも動的なクラス生成が使われている。
こういった動的なクラス生成を担っているのがObjExprで,compileメソッドを呼ぶことでバイトコードを生成できる。このcompileメソッドは,fnやreify, deftypeをanalyzeするときに呼ばれる。なので,たとえば先程のanalyze関数で解析すると,その時点ですでにクラスが生成されている。
user=> (-> (analyze '(fn [] (println "Hello, World"))) :expr :compiled-class) user$fn__1183 user=> (clojure.lang.Reflector/invokeConstructor *1 (object-array 0)) #<user$fn__1183 user$fn__1183@477b1683> user=> (*1) Hello, World nil user=>
ObjExprのサブクラスを作って,適当なメソッド(emitMethodsあたり)をオーバーライドすれば,reifyやdeftypeのような機能を好きなように作れるはず。このあたりをさらに掘り下げるもっとおもしろいことができそう。