夢とガラクタの集積場

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

Clojure勉強日記(その7 2.7 メタデータ

こんにちは。

ついに2章も最後まで来た形になります。
とりあえず概念が大量に出てきたのにたいして書いた量がいまいち不足しているため消化不良な感もありますが・・・
まぁ、それはその先の章でも書き続けることで対処できるでしょう。

では、続きです。

メタデータとだけ聞くとデータの性質を示すデータ・・・などと考えるが、
Clojureにおいては「オブジェクトの論理的な値とは直交するデータ」のことを示すとのこと。
なんじゃこりゃ、と思うが、とりあえず実例を。

例えば人の姓名、年齢というのは昔ながらの単純なデータ。
対して、人オブジェクトがXMLへとシリアライズ出来るかという情報は人そのものとは直接関係がない。
後者のことをメタデータ・・・と言うらしい。
後は人オブジェクトがオンメモリ変更を受けていて、データベースに書き出す必要がある状態を持ってる・・・
というのもメタデータ
つまりは「実際に人そのものの性質を示す以外のデータ」をメタデータというらしい。

ただ、直交するという意味は未だ不明ではあるが。

ともあれ、具体的な使い方を。
下記のようにコレクションにwith-metaを使用すればメタデータを付与できる。
(with-meta object meta)
実際にデータ構造を作り、meta-detaで同じ内容で、メタデータを含むオブジェクトを作成する。

user=> (def student {:name "Student" :email "student@test.com"})
#'user/student
user=> (def serializable-student (with-meta student {:serializable true :status 1}))
#'user/serializable-student

とりあえずtoStringの結果やオブジェクト型、インスタンスの内容を比較するが、全て同じ。
そのため、メタデータをつけることは元々のオブジェクトの内容に影響は与えない模様。

user=> student
{:name "Student", :email "student@test.com"}
user=> serializable-student
{:name "Student", :email "student@test.com"}
user=> (class serializable-student)
clojure.lang.PersistentHashMap
user=> (class student)
clojure.lang.PersistentHashMap
user=> (= student serializable-student)
true

尚、Clojureでは=(イコール)はJavaでのequalsメソッド、つまり内容も含めた等価性を示す。
identical?では実際の参照が同じかどうかを確認可能なため、こちらで確認してみるとfalseとなる。
そのため、メタデータ追加は「元々のオブジェクトをメタデータを示すラッパーでラッピングする」というノリの模様。

user=> (identical? student serializable-student)
false

メタデータにはmetaマクロでアクセス可能とのこと。
実際に試してみるとserializable-studentにのみメタデータが付与されているのがわかる。
後は^というリーダマクロでも参照可能・・・らしいが、現状のClojureREPLではなぜか動作しない。

user=> (meta student)
nil
user=> (meta serializable-student)
{:serializable true, :status 1}
user=> ^serializable-student
)
RuntimeException Unmatched delimiter: )  clojure.lang.Util.runtimeException (Util.java:219)

と思ったら、Clojure1.1では^マクロは非推奨となり、Clojure1.2では#^metadetaという構文でしめされるデータを^metadetaと書けるようになったらしい。
こういう風にバージョンによっても差分はある模様。というかそれはどの言語でも同じだが。
差分が発生している=進化し続けていると前向きにとらえて続きを。

既存のオブジェクトから新しいオブジェクトを創りだした場合、元のオブジェクトに付与されていたデータも引き継ぐ。
下記のようにassoc関数はマップにkey/valueの値を足した新たなマップを返す。
(assoc map k v & more-kvs)
assocを元にserializable-studentから新たなオブジェクトを生成する。

user=> (def student-state (assoc serializable-student :state "state" :flag true))
#'user/student-state
user=> student-state
{:state "state", :name "Student", :flag true, :email "student@test.com"}
user=> (meta student-state)
{:serializable true, :status 1}

で、こうなると試したくなるのが「メタデータを保持したオブジェクト同士を結合したらどーなる?」というもの。
試してみる。

user=> (def account {:id "accountid" :pass "password"})
#'user/account
user=> (def rank-account (with-meta account {:rank 10 :is-active true}))
#'user/rank-account
user=> rank-account
{:pass "password", :id "accountid"}
user=> (meta rank-account)
{:rank 10, :is-active true}
user=> (merge serializable-student rank-account)
{:pass "password", :name "Student", :email "student@test.com", :id "accountid"}
user=> (def merged-map (merge serializable-student rank-account))
#'user/merged-map
user=> merged-map
{:pass "password", :name "Student", :email "student@test.com", :id "accountid"}
user=> (meta merged-map)
{:serializable true, :status 1}

さすがに2つの要素のメタデータ自体はマージされないらしい。
第一引数に指定したmapのメタデータのみを保持するオブジェクトとなっている。
おそらく、mergeの内部実装が
「第一引数に指定したオブジェクトに対して第二引数以降に指定したオブジェクトの要素を全部putする」となっていると思われるが・・・
とまぁ、それはそれで。

1.リーダメタデータ

Clojure自身もメタデータをいくつかの個所で使用しているらしい。
実際var空間にある関数のメタデータをいくつか確認してみる。

user=> (meta #'str)
{:arglists ([] [x] [x & ys]), :ns #<Namespace clojure.core>, :name str, :column
1, :added "1.0", :static true, :doc "With no args, returns the empty string. Wit
h one arg x, returns\n  x.toString().  (str nil) returns the empty string. With
more than\n  one arg, returns the concatenation of the str values of the args.",
 :line 504, :file "clojure/core.clj", :tag java.lang.String}
user=> (:arglists (meta #'str))
([] [x] [x & ys])
user=> (:arglists (meta #'merge))
([& maps])
user=> (meta #'merge)
{:ns #<Namespace clojure.core>, :name merge, :arglists ([& maps]), :column 1, :a
dded "1.0", :static true, :doc "Returns a map that consists of the rest of the m
aps conj-ed onto\n  the first.  If a key occurs in more than one map, the mappin
g from\n  the latter (left-to-right) will be the mapping in the result.", :line
2676, :file "clojure/core.clj"}

こう見ると、docマクロ自体もvarのmetadetaを整形して表示していると思われる。

その上で、独自のkey/valueをvarに追加したければ下記のメタデータリーダマクロが使える。
#^metadeta
・・・ただ、本の中では引数であるargと、返り値に両方メタデータを設定している。
本来は下記のように引数と、返り値別々に定義可能であるように思えるのだが・・・

user=> (defn shout-nometa [arg] (.toUpperCase arg))
#'user/shout-nometa
user=> (defn shout-argmeta [#^{:tag String} arg] (.toUpperCase arg))
#'user/shout-argmeta
user=> (defn #^{:tag String} shout-returnmeta [arg] (.toUpperCase arg))
#'user/shout-returnmeta
user=> (defn #^{:tag String} shout-meta [#^{:tag String} arg] (.toUpperCase arg)
)
#'user/shout-meta

その上で、各々に対してメタデータを参照すると下記のようになる。
返り値にメタデータを設定した場合はvarのメタデータにも表示される。

#'user/shout-meta
user=> (meta #'shout-nometa)
{:arglists ([arg]), :ns #<Namespace user>, :name shout-nometa, :column 1, :line 75, :file "NO_SOURCE_PATH"}
user=> (meta #'shout-argmeta)
{:arglists ([arg]), :ns #<Namespace user>, :name shout-argmeta, :column 1, :line 77, :file "NO_SOURCE_PATH"}
user=> (meta #'shout-returnmeta)
{:arglists ([arg]), :ns #<Namespace user>, :name shout-returnmeta, :column 1, :line 80, :file "NO_SOURCE_PATH", :tag java.lang.String}
user=> (meta #'shout-meta)
{:arglists ([arg]), :ns #<Namespace user>, :name shout-meta, :column 1, :line 81, :file "NO_SOURCE_PATH", :tag java.lang.String}

で、各々に対してtoUpperCaseを使用できない「11111」を渡すと下記のようになる。
つまりはargに対してメタデータを指定した場合は関数呼び出し時にメタデータに指定した引数型へのキャストが行われる模様。
#変換では無い。なぜなら、toStringメソッドJava上で定義されているため変換自体は可能であるだろうから。
とりあえず、これで引数バリデーションのようなものも行える模様。

user=> (shout-nometa 11111)
IllegalArgumentException No matching field found: toUpperCase for class java.lang.Long  clojure.lang.Reflector.getInstanceField (Reflector.java:271)
user=> (shout-argmeta 11111)
ClassCastException java.lang.Long cannot be cast to java.lang.String  user/shout-argmeta (NO_SOURCE_FILE:77)
user=> (shout-returnmeta 11111)
IllegalArgumentException No matching field found: toUpperCase for class java.lang.Long  clojure.lang.Reflector.getInstanceField (Reflector.java:271)
user=> (shout-meta 11111)
ClassCastException java.lang.Long cannot be cast to java.lang.String  user/shout-meta (NO_SOURCE_FILE:81)

だが、これでは引数にメタデータを設定した場合しか働いていない。
そのため、返り値メタデータが有効であるかどうかを数を返そうとする関数を作って確認する。

user=> (defn #^{:tag String} calc [arg] (* arg 2))
#'user/calc
user=> (meta #'calc)
{:arglists ([arg]), :ns #<Namespace user>, :name calc, :column 1, :line 90, :file "NO_SOURCE_PATH", :tag java.lang.String}
user=> (calc 11111)
22222
user=> (class (calc 11111))
java.lang.Long

・・・・うん、キャストされていない。であればメソッドの頭につけるメタデータは返り値メタデータではなく、
metaで参照したときに表示するためだけに用いられるメタデータ・・・ということなのだろう。

尚、:tagメタデータは#^Classnameという表記でも定義可能とのこと。

user=> (defn #^String shout [#^String arg] (.toUpperCase arg))
#'user/shout
user=> (meta #'shout)
{:arglists ([arg]), :ns #<Namespace user>, :name shout, :column 1, :line 94, :file "NO_SOURCE_PATH", :tag java.lang.String}
user=> (shout 11111)
ClassCastException java.lang.Long cannot be cast to java.lang.String  user/shout (NO_SOURCE_FILE:94)
user=> (shout "Test")
"TEST"
user=> (shout :key)
ClassCastException clojure.lang.Keyword cannot be cast to java.lang.String  user/shout (NO_SOURCE_FILE:94)

また、メタデータをまとめて後でおくことも可能。
その場合は下記のようになる。
だが、引数のキャストは行われないため、結局何の意味があるのやら・・・

user=> (defn shout ([arg] (.toUpperCase arg)) {:tag String})
#'user/shout
user=> (meta #'shout)
{:arglists ([arg]), :ns #<Namespace user>, :name shout, :column 1, :line 99, :file "NO_SOURCE_PATH", :tag java.lang.String}
user=> (shout :key)
IllegalArgumentException No matching field found: toUpperCase for class clojure.lang.Keyword  clojure.lang.Reflector.getInstanceField (Reflector.java:271)

ともあれ、オブジェクトにメタデータを付与可能なことと、
リーダメタデータを使うことでコンパイラにコードにメタデータを付与可能なことはわかった。
だが、関数の頭にリーダメタデータで付与するメタデータの意味は・・・
と思いきや、章の最後にこんな記述が。

「原則として、varや引数にメタデータを付与するときには〜」
「:tag 引数または戻り値の期待する値」
なので、関数の頭にリーダメタデータで付与するメタデータはvarにバインドした関数に対して
メタデータを表示した際に表示するもので、実際の動作には影響を与えないのだろう。
いわばJavaでいえばJavaDocのようなもので、動作の記述はするが、実際の動作には影響を与えないという。

・・・どなたか、間違っている、理解が足りないなどありましたら突っ込みくださいーー;

ともあれ、これで2章が終了。
少しはまともにコードが読めるようになったのかもしれませんね。