跳转到内容

学习 Clojure/元数据

来自维基教科书,开放的书籍,开放的世界
Previous page
数据结构
学习 Clojure Next page
特殊形式
元数据

官方元数据文档:https://clojure.net.cn/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 是用户定义的元数据。可以在 https://clojure.net.cn/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) 的文档,它会将我们引导到 https://clojure.net.cn/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/函数,并进行充分测试。

华夏公益教科书