学习 Clojure/元数据
官方元数据文档: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/函数,并进行充分测试。