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

(-> % read write unlearn)

All opinions expressed are solely my own and do not express the views or opinions of my employer.

def と Symbol と Var の話

GPソフト Wiki - ClojureのVarの記事を読んでいて、 自分が、Clojureのdefの挙動やSymbol, Varといった言葉の意味をよく理解していないことに気付きました。この記事や以下の記事を参考にしつつ自分なりに一度整理してみます。

clojure.lang.Varについて知っていること、分かったこと

  • なんらかの「もの」を表すオブジェクト。名前はない(名前の機能はSymbolが独立して担当)。

  • 可変(mutable)。Clojureで可変なのは他に、RefsAgentsAtomsだけ。*1

  • defによって、なんらかのオブジェクトを束縛した状態で作成され、名前空間にinternされる。

  • ClojureVarに似た機能をJavaのなかに探すとしたら「変数」だろうか。

  • Varという名前は、variableからきていると思っている。

clojure.lang.Symbolについて知っていること、分かったこと

  • Rubyのシンボルとはかなり違う。Rubyのシンボルに近い用途としてClojureでよく使うのはKeyword

  • 「もの」の名前を表すオブジェクト。

  • ClojureSymbolに似た機能をJavaのなかに探すとしたら「変数名」だろうか。Javaの場合は「変数」と「変数名」を切り離して捉えたりはあまりしないが、Clojureではこの「変数名」の役割をSymbolというオブジェクトに独立して担わせている。

  • Symbolを評価すると、まずその名前に対応するVarを調べ、そしてVarが束縛する値になる。対応するVarが見つからない場合は、RuntimeExceptionが投げられる。

user=> a

CompilerException java.lang.RuntimeException: Unable to resolve symbol: a in this context, compiling:(/private/var/folders/sf/lyhc_rw16xlckx_yj0gyh5c40000gn/T/form-init1896185904742541191.clj:1:13012)
user=> (def a 123)
#'user/a
user=> a
123
  • SymbolインターフェイスIFnを実装している。つまり関数として振る舞うことができる。
    具体的には、コレクションを1つ引数に取り、そのSymbolのキーに対応するバリューを返す。
user=> ('a {'a 10 'b 20})
10

defについて知っていること、分かったこと

defによるbind(束縛)

オブジェクトを束縛する単純なコードです。

user=> (def foo "hoge")
#'user/foo

このコードでは

  1. まず、"hoge"というStringオブジェクトが生成される。*2
  2. 次に、Varオブジェクトが作成される。
  3. このとき、このVarオブジェクトは"hoge"オブジェクトをbind(束縛)する。
  4. そして、fooという名前のSymbolオブジェクトが作成され、
  5. それから、このVarオブジェクトをintern(拘禁)する。
    具体的には、SymbolVarマッピングを、Namespace名前空間)に登録している。
  (4) Symbol                    (2) Var                   (1) String
+------------+               +------------+             +------------+
|            |               |            |             |            |
|    foo     |-------------->| 0x4e082766 |------------>|    hoge    |
|            |  (5) intern   |            |  (3) bind   |            |
+------------+               +------------+             +------------+

            Namespace
    +-------------------------------+
    |     +-----------------------+ |
    |     | [Symbol] :   [Var]    | |
    |     | ...      : ...        | |
    |     | ...      : ...        | |
    |     | foo      : 0x4e082766 | |  (5) intern
    |     | ...      : ...        | |
    |     +-----------------------+ |
    |                               |
    +-------------------------------+
  • 上記の図のとおり、internの対応はSymbolオブジェクトが保持しているわけではなくて、Namespace名前空間)オブジェクト内のフィールドにマップで保持されています。面倒なので、以降の図ではマッピングの図は省略して単にSymbolからVarに矢印を引きます。

このような挙動なので、多くの他の言語ではvar x = 1の結果を「xは1」と表現するのに対して、Clojureでは同様の(def x 1)というコードは「シンボルxと名前が付けられたvarの値が1」と表現するとやや厳密になります。

defによるunboudなVar

defの第二引数はVarがbindする初期値です。これを省略すると何もbindしない(unboundな)状態のVarがinternされます。 *3 *4

user=> (def bar)
#'user/bar
    Symbol                         Var                   Var$Unbound
+------------+               +------------+             +------------+
|            |               |            |             |            |
|    foo     |-------------->| 0x4e082766 |------------>|            |
|            |    intern     |            |    bind     |            |
+------------+               +------------+             +------------+

この状態でbarを評価するとunboudなVar$Unboundというオブジェクトが返ってきます。

user=> bar
#object[clojure.lang.Var$Unbound 0x62d56f27 "Unbound: #'user/bar"]

user=> (class bar)
clojure.lang.Var$Unbound

ちなみに、自分はこのときのVarは何もbindしていないのかと思っていたのですが、実際には上記の図のようにclojure.lang.Var$Unboundクラスのオブジェクトをbindしているようです。 このclojure.lang.Var$Unboundは、clojure.lang.Varクラス内に書かれたインナークラスであり*5、コードを読むとclojure.lang.Varへの参照のフィールドを持っています。

上記の図よりも下記の図のほうが、実際の実装により近いかもしれません。

    Symbol                         Var                   Var$Unbound
+------------+               +------------+             +------------+
|            |               |            |------------>|            |
|    foo     |-------------->| 0x4e082766 |             |            |
|            |    intern     |            |<------------|            |
+------------+               +------------+             +------------+
(1) そのSymbol自体の取得

そのSymbolオブジェクト自体を取得するにはquote特殊フォームを使います。

user=> (quote foo))
foo
これを取得する
      |
      |
      V

    Symbol                         Var                      String
+------------+               +------------+             +------------+
|            |               |            |             |            |
|    foo     |-------------->| 0x4e082766 |------------>|    hoge    |
|            |    resolve    |            |             |            |
+------------+               +------------+             +------------+

'リーダー・マクロでも同じ効果が得られます。

user=> 'foo
foo

symbol特殊フォームを使うとSymbol名を文字列で指定して取得できます。

(symbol "foo")
foo

'fooの型を確認すると、確かにclojure.lang.Symbolです。

user=> (class 'foo)
clojure.lang.Symbol
(2) intern(拘禁)されているVarの取得

Varオブジェクト自体を取得するにはvar特殊フォームを使います。

user=> (var foo)
#'user/foo
これを取得する
             -----------------------¬
                                    |
                                    V

    Symbol                         Var                      String
+------------+               +------------+             +------------+
|            |               |            |             |            |
|    foo     |-------------->| 0x4e082766 |------------>|    hoge    |
|            |               |            |             |            |
+------------+               +------------+             +------------+

#'リーダー・マクロでも同じ効果が得られます。

user=> #'foo
#'user/foo

#'fooの評価結果の型を確認すると、確かにclojure.lang.Varです。

user=> (class #'foo)
clojure.lang.Var
(3) bind(束縛)されている値の取得

defでbindした"hoge"オブジェクトを取得するには、fooというSymbolを評価します。

user=> foo
"hoge"
  1. まず、fooというSymbolに対応するVarを調べます。これを「resolve(解決)する」と表現するようです。
  2. 対応するVarが見つかったら、そのVarがbindする値を取得する。
  3. この場合、得られる値はhoge文字列オブジェクトです。
これを取得する
             --------------------------------------------------¬
                                                               |
                                                               V

    Symbol                         Var                    (3) String
+------------+               +------------+             +------------+
|            |               |            |             |            |
|    foo     |-------------->| 0x4e082766 |------------>|    hoge    |
|            |  (1) resolve  |            |     (2)     |            |
+------------+               +------------+             +------------+

fooの評価結果の型を確認すると、確かにjava.lang.Stringです。

user=> (class foo)
java.lang.String

また、@リーダーマクロはVarにbindされている値を取得できます。したがって、@#'の2つのリーダー・マクロを組み合わせて"hoge"を得ることもできます。(この場合は読み辛いだけですが。)

user=> @#'foo
"hoge"

まとめ

  1. defは第二引数の値を第一引数のSymbolにintern(拘禁)する。
  2. Symbolを評価すると、そのSymbolにintern(拘禁)されているVarにbind(束縛)されている値が返る。

そして、長くなってきたので続きます。


(追記2016年08月30日)
こちらのまとめもとても勉強になりますので、ぜひご参照を。

togetter.com

*1:Javaのフィールドも含めるべき?

*2:もちろん、"hoge"Stringオブジェクトが作られるのは、正確にはdefの効果ではなく、単に"hoge"というリテラル表現の結果です。

*3:bind(束縛する)の否定の動詞が、unbind(解放する)です。そして、それらの過去形(過去分詞形)がそれぞれ bound と unbound です。

*4:barというシンボルに既になんらかVarがinternされていたら(つまり、barという名前が既に何かでdefされていたら)、defは何もしません。

*5:staticな場合ってインナークラスって言わないんでしたっけ?ごめんなさい、ClojureだけでなくJavaもぼろぼろです。