學習 Clojure/元資料
官方元資料文件:http://clojure.org/metadata
目前,此文件還有很多不足之處。
(meta obj) Returns the meta-data of obj, returns nil if there is no meta-data.
(with-meta obj map) Returns an object of the same type and value as obj, with map as its meta-data.
這看起來很簡單,但並不是全部。在下一節中,我們將介紹元資料的示例。
元資料用於在 Clojure 中描述事物,它無處不在。Clojure 中的所有主要函式都以文件的形式包含元資料。訪問此元資料的兩種方式是透過 (find-doc "...") 和 (print-doc symbol)。這兩個函式的區別在於 find-doc 接受一個字串,通常返回不同函式的文件長列表,而 print-doc 接受一個符號,只打印該符號的文件。
user=> (print-doc find-doc) ------------------------- clojure.core/find-doc ([re-string-or-pattern]) Prints documentation for any var whose documentation or name contains a match for re-string-or-pattern nil
那麼,元資料是什麼樣子的呢?由於這是一本 Clojure 書籍,我們不妨在 Clojure 本身內部看看元資料。Clojure 核心的程式碼可以在 https://github.com/richhickey/clojure/blob/04764db9b213687dd5d4325c67291f0b0ef3ff33/src/clj/clojure/core.clj 找到
核心中的第一段程式碼是 name-space。這是大多數 Clojure 檔案開頭的一段典型程式碼,它是一種防止在共同使用的不同程式碼段中發生名稱衝突的方法。
(ns ^{:doc "The core Clojure language."
:author "Rich Hickey"}
clojure.core)
元資料程式碼是 "^{:doc "The core Clojure language." :author "Rich Hickey"}",程式碼遵循 (symbol ^meta symbol-with-meta) 的形式。此形式的元資料遵循 ^{keyword symbol, keyword symbol} 的模式。此程式碼行中的元資料欄位是 :doc 和 :author,它們應用於 'clojure.core' 符號。^ 在對映之前的字元是一個 reader macro,它擴充套件到 (with-meta)。所以,有了這些知識,你應該能夠用元資料做任何你想做的事。它真的很容易實現,如上所示,而且它似乎很難出錯。但是,讓我們根據在核心和官方文件中看到的內容做一些示例。
在以下示例中,我們將更詳細地探索元資料,並嘗試理解為什麼有時有效而有時無效,以及如何解決元資料難題。首先,讓我們從 meta 函式中獲取文件。
user=> (print-doc meta) ------------------------- java.lang.NullPointerException (NO_SOURCE_FILE:0)
嗯... (print-doc) 確實有效。但是,(print-doc find-doc) 會打印出 (find-doc) 的文件。那麼 'meta' 怎麼回事呢?讓我們嘗試找到這個簡單的程式碼不起作用的原因。
user=> (source meta)
(def
^{:arglists '([obj])
:doc "Returns the metadata of obj, returns nil if there is no metadata."
:added "1.0"}
meta (fn meta [x]
(if (instance? clojure.lang.IMeta x)
(. ^clojure.lang.IMeta x (meta)))))
事實證明,meta 確實以元資料的形式包含文件,也許我們可以嘗試提取它。也許 (print-doc) 中存在 bug。
user=> (meta meta)
{:line 182}
這沒有多大意義。這顯然不同於我們從 (meta) 原始碼中看到的元資料。至少這比 (print-doc) 的空指標異常有所進步。也許 (meta) 壞了,讓我們試著在其他東西上嘗試一下。
user=> (meta find-doc)
{:ns #<Namespace clojure.core>, :name find-doc, :file "clojure/core.clj", :line 3825, :arglists ([re-string-or-pattern]), :added "1.0", :doc "Prints documentation for any var whose documentation or name\n contains a match for re-string-or-pattern"}
看起來 (find-doc) 有很多元資料。它有 :line 元資料,我們在 (meta meta) 中看到過,還有一些其他奇怪的東西,比如 :name 和 :arglists.... 誰會把這些放到他們的元資料中呢?
user=> (source find-doc)
(defn find-doc
"Prints documentation for any var whose documentation or name
contains a match for re-string-or-pattern"
{:added "1.0"}
[re-string-or-pattern]
(let [re (re-pattern re-string-or-pattern)]
(doseq [ns (all-ns)
v (sort-by (comp :name meta) (vals (ns-interns ns)))
:when (and (:doc (meta v))
(or (re-find (re-matcher re (:doc (meta v))))
(re-find (re-matcher re (str (:name (meta v)))))))]
(print-doc v))))
事實證明,這個函式是用 (defn) macro 定義的。這個 macro 使輸入文件元資料的方式不同,它只是函式名稱後面的一個字串,以及主體之前的字串,以及一個對映,它是你要新增的任何其他元資料。然而,只有 3 個元資料片段,所以要麼是 macro 添加了一些,要麼是 Clojure 添加了一些,而我們並不知道。Clojure 可能添加了 :name、:file 和 :line 元資料,而 (defn) 則添加了其他元資料,:doc、:argslist 和 :added 是使用者定義的元資料。可以在 http://clojure.org/special_forms#Special%20Forms--%28def%20symbol%20init?%29 找到 Clojure 編譯器預設新增到物件的元資料的完整列表。
(print-doc find-doc) 工作正常,但 (print-doc meta) 卻不行。(meta meta) 非常奇怪,而 (meta find-doc) 似乎有效。所以,讓我們找出 (meta) 和 (find-doc) 之間的區別。首先,分析一下我們使用 Clojure 時發生的一些事情。
user=> (def string "my string") #'user/string
`user/string` 是我們剛剛建立的符號的 name-space/identifier。由於我們位於 `user` name-space 中,因此可以使用簡寫形式 `string` 來使用此符號。在 'user/string' 的前面有一些奇怪的東西 "#'"。' 字元是一個 reader macro,當它放在符號前面時,表示不進行求值。事實證明,"#'" 是另一個 reader macro,它被替換為 (var symbol)。所以 #'user/string 變成了 (var user/string)。透過 (doc var) 查閱 (var) 的文件,它會將我們引導到 http://clojure.org/special_forms。從這個網站,我們被告知 (var) 是一個特殊形式,這意味著它不是在核心中編寫的,而是語言中的一個公理。
"(var symbol) The symbol must resolve to a var, and the Var object itself (not its value) is returned. The reader macro #'x expands to (var x)."
所以,從這個定義開始,為什麼 (meta meta) 沒有按預期工作變得越來越清楚了。當我們使用 meta 時,它與 #'meta 不同。一個是符號的值,一個是符號本身。我們想要的是符號的元資料,而不是它的值的元資料。讓我們用這些新資訊嘗試一些東西。
user=> meta
#<core$meta clojure.core$meta@2e257f1b>
user=> (var meta)
#'clojure.core/meta
user=> (meta (var meta))
{:ns #<Namespace clojure.core>, :name meta, :file "clojure/core.clj", :line 178, :arglists ([obj]), :doc "Returns the metadata of obj, returns nil if there is no metadata.", :added "1.0"}
所以,現在我們正在從 (meta) 獲取真正的元資料。不過,我們用 (meta meta) 獲取了什麼元資料呢?
user=> (meta meta)
{:line 182}
一個是 :line 178,另一個是 :line 182。這是因為 (def meta) 位於第 178 行,而它指向的值位於第 182 行。(meta) 指向的東西沒有任何元資料,它的元資料是由編譯器生成的,它會為所有東西提供其求值所在行的元資料。所以,現在我們對元資料有了更好的理解,我們可以嘗試一些示例程式碼來測試它。
user=> ^{:doc "a number"} 5
java.lang.IllegalArgumentException: Metadata can only be applied to IMetas
user=> ^{:doc "a number"} "5"
java.lang.IllegalArgumentException: Metadata can only be applied to IMetas
user=> ^{:doc "a number"} :5
java.lang.IllegalArgumentException: Metadata can only be applied to IMetas
所以,看起來我們只能將元資料應用於具有 IMeta 的東西... 到目前為止,我們知道 (def) 會使用 IMeta,但也許還有其他一些東西也使用它?Clojure 定義了一些其他有趣的特殊形式,*1, *2, *3,它們分別返回最後求值的第一個、第二個和第三個東西。以下示例將使用這些。
user=> [1 2 3]
[1 2 3]
user=> *1
[1 2 3]
user=> ^{:doc "a vector"} *1
[1 2 3]
user=> (meta *1)
nil
user=> ^{:doc "a vector"} [1 2 3 4]
[1 2 3 4]
user=> (meta *1)
{:doc "a vector"}
在上面的程式碼中,我們看到了不可變資料的強制執行,因為我無法在 '^{:doc "a vector"} *1' 中的 *1 所引用的向量中新增元資料。但是,最後幾行輸入表明我們可以為匿名物件提供元資料。它也適用於其他集合,而不僅僅是向量。'^{..}' 新增元資料的形式是一個 reader macro,它轉換為 (with-meta),例如
user=> (with-meta '(1 2 3 4 5) {:doc "a list of numbers"})
(1 2 3 4 5)
user=> (meta *1)
{:doc "a list of numbers"}
有時使用 macro 或擴充套件形式更容易閱讀。注意要新增到物件的元資料的位置的不同。
元資料可以有元資料
user=> (def meta-test (with-meta [1 2 3] {:doc (with-meta [4 5 6] {:doc "vec of 4 5 6"})}))
#'user/meta-test
user=> (:doc (meta meta-test))
[4 5 6]
user=> (:doc (meta (:doc (meta meta-test))))
"vec of 4 5 6"
在上面的示例中,我為 (meta-test) 提供了元資料,其中包含一個數組,該陣列又包含它自己的元資料。元資料的巢狀可以建立一些有趣的結構。它可以用於建立資料的版本,或者你可以想到的任何東西。但是,在物件中將元資料用於簡單的標記以外的用途,應該留給 macro/函式,並進行充分測試。