跳轉到內容

學習 Clojure/元資料

來自華夏公益教科書,開放的書籍,開放的世界
Previous page
資料結構
學習 Clojure Next page
特殊形式
元資料

官方元資料文件: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/函式,並進行充分測試。

華夏公益教科書