Clojure中的双冒号::有什么作用?

91

我理解在Clojure中关键字是使用:keyword,但是::是用来做什么的?为什么它看起来像有一个绑定?

user=> :foo
:foo
user=> ::foo
:user/foo
3个回答

93
双冒号的作用是在当前命名空间中完全限定关键字。这旨在避免不同库中有意义的关键字之间发生名称冲突。如果没有完全限定的关键字,您可能会意外地覆盖映射中的某些值,并破坏与库的兼容性。

就提供的理由而言,不太确定为什么一开始会有人去调整图书馆本应处理的地图。如果这确实是主要动机,一些阐述可能会有所帮助。 - matanster
3
一个例子可以是 ring 中间件(类似于 Servlet 过滤器),通过它,您可以丰富描述 HTTP 请求的映射。命名空间键使您能够编写自定义中间件以添加额外的值,而无需担心与在 ring 应用程序中配置的任何其他中间件发生干扰。 - skuro

31
现在已经记录下来,对于Clojure以及对于ClojureScript::关键字也可以用于解决命名空间别名。例如,如果 foo clojure.core 的别名,则 ::foo/bar 将计算为:clojure.core/bar。如果 foo 不能解析为命名空间,则会引发读取器异常。

该功能在以下文档中有详细说明:
  • https://clojure.org/reference/reader#_literals
  • https://cljs.github.io/api/syntax/keyword-qualify
- maxp
1
自Clojure 1.9以来,#:#::语法也很重要,它与映射一起使用以解析映射内的关键字,如此处所述。 - Joe Corneli

0
:: 是一个“完全限定命名空间”(Fully-Qualified NameSpace)的表示,或者我喜欢称之为FQNS。它与FQDN的概念类似。它(::)可以“扩展”为一个“必需别名”(例如,::str是指向(:require [clojure.string :as str])中点部分的别名),或者当前命名空间(例如,::是指向当前文件顶部的(ns myproj.myns ...)的别名)。与不带/的裸键相比,它解析后会有一个/

比较FQNS的形式

FQNS(包含或解析为包含/的形式)可以有几种形式,并且应该与基本的“非限定”关键字语法进行比较,该语法使用单个:而没有/。FQNS的“快捷”关键字语法使用前导冒号(::)。
短格式快捷方式(::k 是 "local",::aa/k 是 "aliased")
这些会根据上述描述展开为完整的命名空间。请将::视为代表被压缩的完整长名称。它们可以包含点,但我认为破折号更清晰。注意后者包含一个/
长格式显式表示(:bb.cc.dd/k
这些是完全拼写出来的,用点和破折号以有意义的方式表示包含键的命名空间。点是低级别(文件系统)的细节,但表示父/子层次结构,其中aa.bb.cc是祖父,bb是父亲,cc是孩子。显式的完全限定命名空间只有一个:,但包含一个/(例如::aa/bb:cc.dd/ee)。
裸露的、基本的、未经修饰的(:k

这是你从第一天就知道的基本不合格的关键。

我发现让编辑器明显地显示出你正在观察的形状是很有帮助的。在屏幕截图中,你可以看到::红色表示当前的NS,显式完整的NS是蓝色,"别名"NS是绿色,最后的"key"是紫色

Emacs screenshot with colored key syntax

完整示例

在这个示例中(您可以在REPL中尝试),请注意这些“解析”(请参见尾部注释)。这里有超过十几种情况,每一种都有所不同。

(ns proj.ns1)
(ns proj.area.ns2)
(ns proj.ns3
  (:require
   [clojure.string   :as str]
   [clojure.data.avl :as d-a]   ; or da or avl or just a; be consistent across code base
   [clojure.data.zip :as d.z]   ; using dots in alias is confusing IMO
   [proj.area.ns2    :as ns2]   ; some fns being used
   [proj.ns1   :as-alias ns3])) ; keying convenience, new in v1.11, avoids circular deps

(def m "A contrived map demonstrating various key shapes"
  {:aa               "good"  ;=> :aa                      ; typical: basic unqualified key
   ::bb              "good"  ;=> :proj.ns3/bb             ; typical: note that / is implicitly added
   ::str             "bad"   ;=> :proj.ns3/str            ; missing key
   :ns3/cc           "weird" ;=> :ns3/cc                  ; missing full ns despite alias
   :proj.area.ns4/dd "ok"    ;=> :proj.area.ns4.dd        ; ns might not exist but ok
   ::ns2/ff          "good"  ;=> :proj.area.ns2/ff        ; common practice
   :proj.area.ns2/gg "ok"    ;=> :proj.area.ns2/gg        ; but this is why we have `:as-alias`
   :proj.ns1/hh      "bad"   ;=> :proj.ns1/hh             ; clearer to just use `::` for cur ns
   :str/ii           "bad"   ;=> :str/ii                  ; an accident: str not expanded
   ::str/jj          "good"  ;=> :clojure.string/jj       ; and typical
   ::kk.ll           "so-so" ;=> :proj.ns3/kk.ll          ; confusing to have dots in actual key
   ::d-a/mm.nn       "so-so" ;=> :clojure.data.json/mm.nn ; again, dots in key
   ::d-a/oo          "good"  ;=> :clojure.data.json/oo    ; typical
   ::d.z/pp          "so-so" ;=> :proj.ns3/pp             ; dots in qualifier diff from ns structure
   :random/qq        "good"  ;=> :random/qq               ; qualified, but not a real ns
   :other.random/rr  "good"  ;=> :other.random/rr         ; qualified, but not a real ns
   })

请注意,这些都是你正在“创建”的新键。实际上,:jj键在clojure.string命名空间中不存在,但这并不妨碍你使用它。

精简版

其中最令人惊讶的一点是,在本地使用(::)中扩展时添加了/。因此,这三个是需要记住的重点内容。
WRITTEN     RESOLVED
:aa      => :aa (bare)
::bb     => :proj.ns1/bb (qualified)
::ns3/cc => :proj.ns3/cc (qualified, slash explicit)

在左边:
- `aa` 和 `bb` 看起来几乎一样,但解析方式完全不同 - `bb` 和 `cc` 看起来很不一样,但实际上解析方式非常相似
我有点希望使用 `::/foo` 而不是 `::foo`,因为后者中的隐式 `/` 使系统感觉不规则。
当你使用像 mallispecintegrant 这样的库时,了解大多数 FQNS(Fully Qualified Namespace Symbol)情况非常重要,因为它们广泛使用 FQNS。对于任何规模较大的项目,通常需要限定一些键以避免冲突。
解构赋值
请注意在这些解构赋值中使用的 `:keys` 和选择符 `:`、`::` 以及无选择符的情况。
(let [{:keys [aa]} m]               aa) ; "good" (typical)
(let [{:keys [:aa]} m]              aa) ; "good" (also works with :)
(let [{:keys [::aa]} m]             aa) ; nil
(let [{:keys [::bb]} m]             bb) ; "good"
(let [{:keys [ns2/ff]} m]           ff) ; nil
(let [{:keys [:ns2/ff]} m]          ff) ; nil
(let [{:keys [::ns2/ff]} m]         ff) ; "good"
(let [{:keys [ns3/cc]} m]           cc) ; "weird"
(let [{:keys [:ns3/cc]} m]          cc) ; "weird"
(let [{:keys [other.random/rr]} m]  rr) ; "good"
(let [{:keys [:other.random/rr]} m] rr) ; "good"

结论

这是 Clojure 语法中一个棘手的领域,但它是日常代码的重要组成部分。如果你遵循类似下面这些 NS 风格指南,可以帮助减少混乱:

请注意,关于别名约定(str vs string,别名中的点等)可能存在争议,但关键是在你的规则上确立和保持一致。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接