この記事は、lispリーダーマクロアドベントカレンダー の4日目の記事です。 タイトルにある通り、Clojure でのリー ダーマクロについて取り扱います(対象とする Clojure のバージョンは 1.4)。
リーダーマクロの定義等については、このアドベントカレンダー初日の記事 #:g1: Lispのリーダーマクロとはなにか に詳しく書かれておりますのでそちらをご参照願います。ここでは Clojure でのリーダーマクロということなので、他の Lisp処理系との違いがなるべく 伝わるように書いていきたいと思います。なるべく網羅的に書こうと思います ので、どちらかといえば Clojure 初級者向き(というか知らない人向け)の記 事にしてみました。
コメントは普通の Lisp 処理系と同じく ;
(セミコロン)から行末まで、にな
ります。
また、comment
という常に nil を返すマクロがあるので、適当なコードの塊
をまとめてコメントアウトするときには便利かもしれません。
(comment
(defun hoge [] (fuga))
)
;=> nil
同様の機能として、後述するディスパッチマクロの中に Discard マクロとい
うものがあります。これは、form の前に #_
をつけることで、form を「丸
ごと無視」するという働きをします。
#_(defun hoge [] (fuga))
comment
マクロよりも#_
のほうが実用的ではないかと思います。
ただ、comment
マクロも #_
マクロも、form が reader でちゃんと読め
る(エラーにならない)ことが前提です。カッコがアンバランスな文字列をコメン
トに入れたい、とかいう場合には、素直に ;
でコメントアウトしましょう。
(comment (this is a) comment) ; => nil
(comment this is a) comment) ; => CompilerException java.lang.RuntimeException: ...
#_( this is a) ) ; => RuntimeException Unmatched delimiter: ) ...
; this is a) comment ; => これはエラーにならない
あと、#!
から行末までもコメントとして認識されます(スクリプトを意識し
ているのかもしれません)。
多くのプログラミング言語と同様、 文字列 の始まりを表します。ダブル クオートで囲まれた範囲が文字列となります。
これも多くのプログラミング言語と同様、\
の直後にある文字そのものを表
します。
\a ; => a という文字そのもの
(class \a) ; => java.lang.Character (java版Clojureの場合です、もちろん)
Lisper ならおなじみの quote
、これは普通の Lisp 処理系と同じく '
(シングルクオート)です。直後の form の eval を抑制します。
'a ; => (quote a)
いわゆる Lisp でいうところの List は、(
)
で表現されます。フツーで
すね。
(1 2 3)
(1 (2 (3 (4 (5 6)))))
Clojure には、vector、map、set、という、リスト以外にもコレクションを取 り扱う型が標準で用意されており、またリーダーが適切に取り扱えるようマク ロ文字が定義されております。
いわゆる配列です。[
]
で表現されます。
[1 2 :a]
Clojure では ,
は空白文字と同義ですので、
(= [1 2 :a] [1, 2, :a]) ; => true
です。マクロ文字 [
]
を使わずに書こうとすると、vec
関数を使って、
(vec (list 1 2 :a))
のような感じになります。
キーと値を対応付ける、Javaでいうところの HashMap 相当のコレクションで
す。{
}
で表現されます。
{:a 1 :b 2 :c 3}
;; カンマを入れると区切りが目立つ
{:a 1, :b 2, :c 3}
これもマクロ文字{
}
を使わずに書こうとすると、hash-map
関数を使って、
(hash-map :a 1 :b 2 :c 3) ; => {:a 1, :b 2, :c 3}
のような感じになります。
Javaでいうところの HashSet のようなものです。重複を許さない値の集合、
といったところでしょうか。#{
}
で表現されます。
#{:a :b 3} ; => #{:a :b 3} ; 順番は意識しない
これもマクロ文字#{
}
を使わずに書こうとすると、hash-set
関数を使って、
(hash-set :a :b 3) ; => #{3 :a :b}
のような感じになります。[]
、{}
、#{}
、簡単に書けていいですね。
#
につづけて文字列を書くと、その文字列は正規表現リテラルとして扱われ
ます。正規表現パターンは、re-pattern
関数で作成できますが、いろいろ
エスケープする必要がある場合が多いので、#""
で書くと簡潔に記述するこ
とができます。
(re-seq (re-pattern "\\(list a b c\\)") "(list a b c)") ; => ("(list a b c)")
(re-seq #"\(list a b c\)" "(list a b c)") ; => ("(list a b c)")
上はどちらも同じパターンマッチをさせていますが、#"
を使った方が \
が少なくすっきり見えます。
Clojure の特徴として並行処理のサポートがありますが、その機能の一つとし
て Ref
というものがあります。
(def ref-sample (ref 0))
ref-sample ; => #<Ref@737c2891: 0>
この ref
の実体を取得するには @varname
という書式を使用します。
@ref-sample ; => 0
実は @
はただのマクロ文字で、(deref varname)
のように展開されます。
(※実際には「展開されたような動きをする」です)
(deref ref-sample) ; => 0
この他にも、Clojure には form の 評価を遅らせる ためのマクロとして
delay
というものがあり、遅らせたその評価を強制的に実行させる force
という関数があるのですが、deref
は force
の代わりとしても使えるの
で、delay
に対しても @
構文で値を参照することができます。
- 普通に
force
を使う例
(def foo (delay 0))
foo ; => #<Delay@4b85c17: :pending>
(force foo) ; => 0
foo ; => #<Delay@4b85c17: 0> 状態が変わった!
@
でも同じことができます。
(def bar (delay 0))
bar ; => #<Delay@47ef7de4: :pending>
@bar ; => 0
bar ; => #<Delay@47ef7de4: 0> 状態が変わった!
Clojure では、変数や関数の実体への参照として var というものがあります。
def
やdefn
で定義するものは、変数や関数そのものと、それを参照する
var になります。
Clojure では、var に対して メタデータ を付けることが出来ます。
メタデータは、コンパイル時に参照される型情報だったり、関数や変数定義に 対するドキュメントだったりします。以下に少し例を示します。
(def ^:private foo "private var") ; :private true というメタデータを追加
(def ^{:private true} foo "private var") ; 上と全く同じ
メタデータは meta
関数で参照できます。
(meta foo) ; => nil
ただし、上記のように普通に var に対して呼び出してしまうと、nil になっ
てしまいます。上記の書き方では foo
というシンボルが eval された結果
が meta
関数に渡されるので、せっかく foo
という var に付けたメタデー
タを参照することができません。そこで、引数 form の var そのものを返す
var
という特殊形式に登場してもらいます。
(meta (var foo)) ; => {:ns #<Namespace user>, :name foo, :private true, :line 103, :file "NO_SOURCE_PATH"}
こんどはちゃんと foo のメタデータを参照できました。ところで、これには
もう一つの書き方があります。それが次に示すリーダーマクロ #'
です。
#'foo
と書くと、(var foo)
のように取り扱いされます。すなわち、foo
のメタデータを取得するには、
(meta #'foo) ; => {:ns #<Namespace user>, :name foo, :private true, :line 103, :file "NO_SOURCE_PATH"}
のように記述すればよいわけです。すっきり書けますね。var
特殊形式につ
いては、 @atos0220さん の記事 Lisp Advent Calendar 2012 1日目
の先頭あたりに詳しく書かれています。
あと、メタデータは関数のパラメータの型ヒントとしても指定できます。
;; 関数の例(引数の型 hintに ^ を使用している例)
(defn- foo [^long i ^long j] (+ i j))
この foo
のメタデータを meta
関数で表示させても、引数の型ヒントのと
ころは残念ながらパッと見はわかりません。
(meta #'foo) ; {:arglists ([i j]), :ns #<Namespace user>, :name foo, :private true, :line 140, :file "NO_SOURCE_PATH"}
こういう場合は、 id:t2ruさん のこのページ
にある ppm
という関数(メタデータを含めたPretty Print)で表示させてみると
(ppm #'foo)
^{:arglists ([^{:tag long}i ^{:tag long}j]),
:ns #<Namespace user>,
:name foo,
:private true,
:line 140,
:file "NO_SOURCE_PATH"}#<Var@3315a56d: #<user$foo user$foo@56459b78>>
nil
のように表示されるので、ちゃんとメタデータとして認識されているようです。
あと、メタデータを付けるためのリーダーマクロとして #^
というのが使え
るようです(が、^
との使い分けがよくわかりませんでした)。
` (バッククオート) は、Lisp で言うところのシンタックスクオートと呼ば れるものです。マクロを定義する際によく使いますが、マクロ定義時以外でも 使う場合もあります。
まず、list、vector、map、set、のリテラルについては、そのまま展開します。
`[1 2 3 4] ; => [1 2 3 4]
`(1 2 3 4) ; => (1 2 3 4)
`{1 2 3 4} ; => {1 2, 3 4}
`#{1 2 3 4} ; => #{1 2 3 4}
シンボルについては、その名前空間を補完します。また、シンボル名の終わり
に #
をつけると、auto-gensym(自動的にユニークなシンボル名をもつシン
ボルを作成)します。
`String ; => java.lang.String
`foo ; => user/foo
`foo# ; => foo__130__auto__
`(+ x y#) ; (clojure.core/+ user/x y__133__auto__)
マクロを定義する際には、シンタックスクオートされているコンテキストの中
で、より外側のコンテキストのシンボルを参照したくなりますが、そういった
場合には ~
を使ってアンクオートします。
`(let [x 1] `(+ ~x y#) ; => (clojure.core/+ 1 y__146__auto__)
最後に、シンタックスクオートされた中のコンテキストに、リストの塊ではな
くバラけた形で渡したい場合は、@~
を使います。日本語では説明しづらい
ので例を下に示します。
(let [x '(1 2)] `(+ ~x)) ; => (clojure.core/+ (1 2)) 動かない!
(let [x '(1 2)] `(+ ~@x)) ; => (clojure.core/+ 1 2)
x
の中身は (1 2) というリストなので、~x
はそのままだと (1 2) に展
開されますが、~@
を使うことで、複数のパラメータにバラけた形で展開さ
れたのがわかります。
Clojure では無名関数を定義する特殊形式はfn
となります(実は局所的な名
前も付けれますが)。無名関数は、関数型言語ではしょっちゅう使うので、簡
潔に記述できればありがたいものです。
(map (fn [x y] (+ x y)) '(1 2 3 4 5) '(6 7 8 9 10)) ; => (7 9 11 13 15)
上の例を、#() と % を使うと、次のように書けます。
(map #(+ % %2) '(1 2 3 4 5) '(6 7 8 9 10)) ; => (7 9 11 13 15)
2番目の例では、map に渡す各要素毎に適用する関数を無名関数 #() で表し、 そのコンテキストの中での引数を、先頭から %、%2、 %3、... というように 参照しています。
ただし、#() はネストできないのでその点だけは要注意です(エラーが出るの で簡単にわかりますが...)。
これは #=
に続く form を評価します。
#=(+ 1 2 3) ; => 6
ただし、変数 *read-eval*
が false になると、上記式はエラーになります。
(read-string "#=(+ 1 2 3)") ; => 6
(binding [*read-eval* false] (read-string "#=(+ 1 2 3)")) ; => RuntimeException EvalReader not allowed when *read-eval* is false. ...
この文字が来るとリーダーは例外を Throw します。
#< ; => RuntimeException Unreadable form ...
Java版 Clojure は Java VM 上で稼働する言語であり、Clojure の reader は Javaで書かれています(少なくとも現時点では)。Clojure の reader 機能を実 現するJavaのクラスは、clojure.lang.LispReader となっていて、ソースは ここ にあります。
LispReader クラスにある read
メソッドが reader 本体ですが、その中で
いくつかの記号については各々の表記法に応じた ほげReader というクラス
(LispReader の inner クラス)に処理させるよう、記号と処理クラスの対応表
(ソースでは macros
という static配列)があります。
macros['"'] = new StringReader();
macros[';'] = new CommentReader();
macros['\''] = new WrappingReader(QUOTE);
macros['@'] = new WrappingReader(DEREF);//new DerefReader();
macros['^'] = new MetaReader();
macros['`'] = new SyntaxQuoteReader();
macros['~'] = new UnquoteReader();
macros['('] = new ListReader();
macros[')'] = new UnmatchedDelimiterReader();
macros['['] = new VectorReader();
macros[']'] = new UnmatchedDelimiterReader();
macros['{'] = new MapReader();
macros['}'] = new UnmatchedDelimiterReader();
// macros['|'] = new ArgVectorReader();
macros['\\'] = new CharacterReader();
macros['%'] = new ArgReader();
macros['#'] = new DispatchReader();
単一のマクロ文字については、上記 macros
配列を見て、各々に対応する
ほげReader() を見ていけばよさそうです。
なお、ソースコード上の '|' については未調査です(コメントアウトされてい る以上機能していないのは良いのですが、なぜコメントアウトされたのか、と かどういう経緯で云々については調べてません)。ご存知の方がいらっしゃい ましたら教えていただけると助かります。
これまで見てきた Clojure のリーダーマクロのうち、#
で始まるものにつ
いては、ディスパッチマクロとして dispatchMacros という配列で処理が分岐
する仕組みになっています。
dispatchMacros['^'] = new MetaReader();
dispatchMacros['\''] = new VarReader();
dispatchMacros['"'] = new RegexReader();
dispatchMacros['('] = new FnReader();
dispatchMacros['{'] = new SetReader();
dispatchMacros['='] = new EvalReader();
dispatchMacros['!'] = new CommentReader();
dispatchMacros['<'] = new UnreadableReader();
dispatchMacros['_'] = new DiscardReader();
Clojure 1.4 より前は特殊なことをしない限りマクロ文字をいじることができ ませんでした。逆引きClojure のこのページ や、id:nokturnalmortumさんのこのページ にサンプルがありますが、どちらも LispReader.java の dispatchMacros を 無理やり書き換えることで実現しています。
ところが、Clojure 1.4 からは、標準の機能としてディスパッチマクロを独自
に定義できるようになりました。具体的には、ルートのクラスパスに
data_readers.clj
というファイルを置き、その中に #
に続く文字列とそ
れに対応する関数を記述し、その関数の実体を別途実装する、というやり方で
す。
これに早速トライされた @uochanさんのページ には、
- #upper 文字列を大文字にする
- #?= デバッグプリント
- #str 文字列中にあるS式を展開する
といった、簡潔で理解しやすい実例が載っておりますので参考になります。 (あともうひとかた、サンプル実装されていたはずなのですが、ちょっと見つ けられませんでした)
今回、リーダーマクロに焦点を当てて、わりと初心者向けに解説を試みてみま した。この文書が Clojure コミュニティを少しでも広げることに役立つこと ができれば幸いです。
明日は sirohuku さんの話です。
参考に読ませて頂きました。
細かいですが1箇所だけ誤植を見つけたのでお知らせします。
Unquote-splicing の
~@
を@~
と誤記している箇所があるようです。