Skip to content

Instantly share code, notes, and snippets.

@ponkore
Created December 3, 2012 15:32
Show Gist options
  • Save ponkore/4195740 to your computer and use it in GitHub Desktop.
Save ponkore/4195740 to your computer and use it in GitHub Desktop.
Clojure のリーダーマクロについて (lisp reader macro advent calendar 2012 の記事です)。

Clojure のリーダーマクロについて

この記事は、lispリーダーマクロアドベントカレンダー の4日目の記事です。 タイトルにある通り、Clojure でのリー ダーマクロについて取り扱います(対象とする Clojure のバージョンは 1.4)。

はじめに

リーダーマクロの定義等については、このアドベントカレンダー初日の記事 #:g1: Lispのリーダーマクロとはなにか に詳しく書かれておりますのでそちらをご参照願います。ここでは Clojure でのリーダーマクロということなので、他の Lisp処理系との違いがなるべく 伝わるように書いていきたいと思います。なるべく網羅的に書こうと思います ので、どちらかといえば Clojure 初級者向き(というか知らない人向け)の記 事にしてみました。

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の場合です、もちろん)

quote

Lisper ならおなじみの quote、これは普通の Lisp 処理系と同じく ' (シングルクオート)です。直後の form の eval を抑制します。

'a        ; => (quote a)

リスト

いわゆる Lisp でいうところの List は、( ) で表現されます。フツーで すね。

(1 2 3)
(1 (2 (3 (4 (5 6)))))

vector、map、set

Clojure には、vector、map、set、という、リスト以外にもコレクションを取 り扱う型が標準で用意されており、またリーダーが適切に取り扱えるようマク ロ文字が定義されております。

vector

いわゆる配列です。[ ] で表現されます。

[1 2 :a]

Clojure では , は空白文字と同義ですので、

(= [1 2 :a] [1, 2, :a])  ; => true

です。マクロ文字 [ ] を使わずに書こうとすると、vec関数を使って、

(vec (list 1 2 :a))

のような感じになります。

map

キーと値を対応付ける、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}

のような感じになります。

set

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 という関数があるのですが、derefforce の代わりとしても使えるの で、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 というものがあります。 defdefnで定義するものは、変数や関数そのものと、それを参照する 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 さんの話です。

@k28abo47gw
Copy link

参考に読ませて頂きました。

細かいですが1箇所だけ誤植を見つけたのでお知らせします。
Unquote-splicing の ~@@~ と誤記している箇所があるようです。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment