夢とガラクタの集積場

落ちこぼれ三流エンジニアである管理人の夢想=『夢』と、潰えた夢=『ガラクタ』の集積場です。

Clojure勉強日記(その4 2.4 var、束縛、名前空間

では、続きです。

オブジェクトをdefやdefnで定義するとオブジェクトは指定した名前にバインドされる。
下記の例ではuser/fooに10がバインドされる。

user=> (def foo 10)
#'user/foo
user=> foo
10
user=> user/foo
10

ここで束縛される場所は「var」と呼ばれる場所にバインドされる模様。
その上で、上記の「var」はルート束縛と呼ばれるとのこと。
varにはスレッドローカルの値をバインドすることも可能だが、それはまた今度。
下記のようなコードでvarに直接アクセスすることもできる。

user=> (var foo)
#'user/foo
user=> #'foo
#'user/foo
user=> #'user/foo
#'user/foo

varはつまりは「あるバインドした変数に対するポインタの名称」のようなもの?
ただ、実際使うことはほとんどないらしい。
varにはいろんな機能があるらしいので、この後確認する形になる。

1.束縛

varは名前を指定してバインドするが、他の種類のバインドもある。
関数を呼び出すと仮引数の名前と実引数の値がバインドされる・・・とある。
とりあえずやってみる。

user=> (defn triple [number] (* 3 number))
#'user/triple
user=> (triple 10)
30
user=> number
CompilerException java.lang.RuntimeException: Unable to resolve symbol: number i
n this context, compiling:(NO_SOURCE_PATH:0:0)

・・・一瞬、関数内でバインドされた内容が関数実行後も有効なのかと思いきや、そんなことはなく。
単に関数内部においてnumberに10という値がバインドされるということのみのよう。

で、説明を読み進めてみるとわかったが、関数の引数バインドは
「レキシカルなスコープ」=関数本体の中だけ見えるということらしい。うん、納得。

同様のバインドを行う書式としてlet形式がある。

(let [bindings*] exprs*)

bindingsはexprsの中で有効になる。
exprsの最後の式の値がlet形式の値となる・・・って、これどういうことかわからない(汗

正方形の頂点の値を底辺の位置bottom、左辺の位置left、一辺の長さsizeから求める関数square-cornerを
let形式を使ってバインドして求めると下記。

user=> (defn square-corner [bottom left size]
    (let [top (+ bottom size) right (+ left size)]
        [[bottom left] [top left] [top right] [bottom right]]))
#'user/square-corner
user=> (square-corner 2 3 5)
[[2 3] [7 3] [7 8] [2 8]]

・・・・!
とりあえず、Java屋さんとしては気になってしまうのは返り値の値。
これ何返しているんでしょう。
ただ、exprsの最後の式の値『[[bottom left] [top left] [top right] [bottom right]]』が返り値となったのは確か。

というわけで試してみた。

user=> (def corner (square-corner 2 3 5))
#'user/corner
user=> corner
[[2 3] [7 3] [7 8] [2 8]]
user=> (class corner)
clojure.lang.PersistentVector
user=> (str corner)
"[[2 3] [7 3] [7 8] [2 8]]"

とりあえず、[[bottom left] [top left] [top right] [bottom right]]という形式を取ると
clojure.lang.PersistentVectorのインスタンスとして返される模様。

また、入れ子では無いパターンも試してみた。

user=> (def multi [[1 2] [2 3]])
#'user/multi
user=> (class multi)
clojure.lang.PersistentVector
user=> (def single [1 3])
#'user/single
user=> (class single)
clojure.lang.PersistentVector
user=> (str multi)
"[[1 2] [2 3]]"
user=> (str single)
"[1 3]"

入れ子であってもPersistentVectorであることには変わらない模様。

ちなみにmapを試してみるとこんな感じ。mapは「PersistentHashMap」というクラスでバインドされる。

user=> (def maptest {:Key :Value})
#'user/maptest
user=> (class maptest)
clojure.lang.PersistentHashMap
user=> (str maptest)
"{:Key :Value}"

後は要素数が違ったパターンなども試したが、とりあえずなんでも入る。
その上で入れ子にもできた。内容が入れ子になった場所だけさらに個別にPersistentVectorが生成されていた。
入れ子でない個所はLong値となっていた。
そのため、JavaでいうObjectの配列を示すのがPersistentVector・・・のように思われる。
かつ、記述で自動的に入れ子になる模様。

user=> (def vectestmulti [[1 3 3 3 3] 1 2 [3 4]])
#'user/vectestmulti
user=> (str vectestmulti)
"[[1 3 3 3 3] 1 2 [3 4]]"
user=> (class vectestmulti)
clojure.lang.PersistentVector
user=> (vectestmulti 1)
1
user=> (vectestmulti 3)
[3 4]
user=> (class (vectestmulti 3))
clojure.lang.PersistentVector
user=> (class (vectestmulti 2))
java.lang.Long
user=> (class (vectestmulti 1))
java.lang.Long
user=> (class (vectestmulti 0))
clojure.lang.PersistentVector

・・・うん、少し満足。

2.分配束

多くの言語ではコレクションの一部にアクセスしたい場合、まずはそのコレクション全体をバインドする。
Clojureでは下記の通り。

user=> (defn greet-author [author]
    (println (str "Hello," (:first-name author))))
#'user/greet-author
user=> (greet-author {:last "Last" :first-name "First"})
Hello,First
nil

ただ、実際に使っているのは「:first-name」の要素のみのため、コレクション全体を「author」にバインドする必要はない。
コレクションの要素のみを取得してバインドすることがClojureでは可能で、バインドする場合、下記のように記述する。
これを分配束縛、と言う模様。

user=> (defn greet-author-sub [{authorname :first-name}]
    (println (str "Hello," authorname)))
#'user/greet-author-sub
user=> (greet-author-sub {:last "Last" :first-name "First"})
Hello,First
nil

要は無名の変数である状態のうちにauthornameを取得してバインドしてしまっているようなノリ。
ここではMap要素で確認しているが、配列を用いると下記のようになる。
今回の場合は1要素ではなく、「2要素目まで」を取りだす記述にしてみる。

user=> (let [[arg1 arg2] [4 3 2 1]] (str arg1 " " arg2))
"4 3"
user=> (let [[arg1 arg2] [4 [3 3] 2 1]] (str arg1 " " arg2))
"4 [3 3]"

頭を飛ばしたい場合は下記のように記述する。尚、「_」は慣用的にバインドを気にしないシンボルとして用いられる模様。
特に特殊な機能を保持しているわけではないらしい。(実際、下記では普通に値がバインドされている。)

user=> (let [[_ arg2 _ arg4] [4 [3 3] 2 1]] (str arg2 " " arg4))
"[3 3] 1"
user=> (let [[_ arg2 _ arg4] [4 [3 3] 2 1]] (str arg2 " " arg4 " " _))
"[3 3] 1 2"

コレクション自体と要素を同時にバインドすることも可能。その場合は「:as」節を用いる。
ただ、:as節以降のバインドについては行われない模様。
そのため、要素のバインド → as節のバインド と記述する必要がある。
まぁ、そうでないとわけわからなくなりますので、その辺は納得。

user=> (let [[_ arg2 _ arg4 :as all] [4 [3 3] 2 1]] (str arg2 " " arg4 " " _ " " all))
"[3 3] 1 2 [4 [3 3] 2 1]"
user=> (let [[_ arg2 _ :as all arg4] [4 [3 3] 2 1]] (str arg2 " " arg4 " " _ " " all))
CompilerException java.lang.RuntimeException: Unable to resolve symbol: arg4 in this context, compiling:(NO_SOURCE_PATH:20:46)

3.名前空間

前述のルート束縛は「名前空間」の中にある。
REPLを起動した後のデフォルトの名前空間は「user」となっている。
user上で定義したシンボルはuser配下に配置される。

user=> (def foo 10)
#'user/foo
user=> (resolve 'foo)
#'user/foo
user=> (resolve "foo")
ClassCastException java.lang.String cannot be cast to clojure.lang.Symbol  clojure.core/ns-resolve (core.clj:3954)
user=> (resolve foo)
ClassCastException java.lang.Long cannot be cast to clojure.lang.Symbol  clojure.core/ns-resolve (core.clj:3954)

尚、シンボルはシンボルで渡す必要がある模様。
「文字列リテラル」と、「シンボル」、値そのもので各々で違うというわけではある。
また、階層を含めたシンボルに対してもバインドできない。

user=> (def foo/test 10)
CompilerException java.lang.RuntimeException: Can't refer to qualified var that doesn't exist, compiling:(NO_SOURCE_PATH:32:1)
user=> (def user/foo/test 10)
CompilerException java.lang.RuntimeException: Can't refer to qualified var that doesn't exist, compiling:(NO_SOURCE_PATH:33:1)

名前空間を切り替えるには「in-ns」を用いる。
こちらもシンボルで指定する必要がある模様。その上で、階層を指定した状態でもOK。

user=> (in-ns test)
ClassCastException clojure.core$test cannot be cast to clojure.lang.Symbol  clojure.lang.RT$1.invoke (RT.java:237)
user=> (in-ns 'test)
#<Namespace test>
test=> (in-ns 'test/test1)
#<Namespace test/test1>

こうなると前に使えていたfooもフルパスを指定しないと使えないようになる。
この辺はJavaと同じのため特に抵抗感はなし。

test/test1=> foo
CompilerException java.lang.RuntimeException: Unable to resolve symbol: foo in this context, compiling:(NO_SOURCE_PATH:0:0)
test/test1=> user/foo
10

尚、名前空間を移動した際にuser名前空間では元々読み込んであったClojureのcoreの要素も使えなくなっている。

test/test1=> use
CompilerException java.lang.RuntimeException: Unable to resolve symbol: use in this context, compiling:(NO_SOURCE_PATH:0:0)

この場合は一度useをフルパスで指定し、clojure.core配下を再度use宣言する。

test/test1=> (clojure.core/use 'clojure.core)
nil
test/test1=> use
#<core$use clojure.core$use@5381062e>

Clojureを読み込んだ時点でjava.langパッケージ配下は自動的に全て使える。
ただし、それ以外のパッケージ配下はフルパスで指定する必要がある。

test/test1=> String
java.lang.String
test/test1=> File
CompilerException java.lang.RuntimeException: Unable to resolve symbol: File in
this context, compiling:(NO_SOURCE_PATH:0:0)
test/test1=> java.io.File
java.io.File

java.langパッケージ配下以外のクラスを使用する場合、インポートすることで名称を短く扱うことができる。

user=> (import '(java.io InputStream File))
java.io.File
user=> File
java.io.File
user=> InputStream
java.io.InputStream

尚、importはJavaクラスにのみ実行可能。Clojureに対して行う場合は「use」で読み込む必要がある。

user=> (require 'clojure.contrib.math)
nil
user=> (clojure.contrib.math/round 1.8)
2
user=> (use 'clojure.contrib.math)
nil
user=> (round 1.9)
2

ただ、こうすると現状の名前空間にmathの全ての関数がバインドされることになる。
そうすると意図しない動作を招くこともある。
読み込んだ名前空間の全ての関数を知っているわけでもないわけだから。
そのため、useに:onlyオプションを渡すことで必要な関数のみをバインドすることが可能。

user=> (use '[clojure.contrib.math :only (round)])
nil
user=> (round 1.5)
2
user=> (round 1.1)
1

REPL上でライブラリの更新を反映したい場合はuseに:reloadオプションを渡すことで可能。
:reload-allオプションを付ければuseに指定した関数が使用している名前空間も一式リロードされる

user=> (use :reload '[clojure.contrib.math :only (round)])
nil
user=> (use :reload-all '[clojure.contrib.math :only (round)])
nil

尚、コードの冒頭で名前空間を指定し、他の名前空間をいくつかuseするには下記のように書く。
(ns name & references)
referencesにはimport、require、useのオプションが指定可能で、
それぞれ同様の関数を実行したときと同じ動作をする。
実際にStormのコードにも下記のような呼び出しが冒頭にある。(acker.clj)

(ns backtype.storm.daemon.acker
  (:import [backtype.storm.task OutputCollector TopologyContext IBolt])
  (:import [backtype.storm.tuple Tuple Fields])
  (:import [backtype.storm.utils RotatingMap MutableObject])
  (:import [java.util List Map])
  (:import [backtype.storm Constants])
  (:use [backtype.storm config util log])
  (:gen-class
   :init init
   :implements [backtype.storm.task.IBolt]
   :constructors {[] []}
   :state state ))

いくつかわからない記述もありますが、まぁそれはおいおい。