Reputation: 1381
I understand keywords in Clojure being :keyword
. But what is the ::
used for? Why does it look like it has a binding?
user=> :foo
:foo
user=> ::foo
:user/foo
Upvotes: 93
Views: 11973
Reputation: 10274
The ::
is for a "Fully-Qualified NameSpace", or FQNS as I like to call
it, similar in concept to a
FQDN. It (::
)
"expands" to either a "required alias" (eg, ::str
is an alias to the dotted
part of (:require [clojure.string :as str])
), or the current NS it's in (eg,
::
is an alias to the (ns myproj.myns ...)
at the top of the current
file). It resolves to having a /
, as compared to a bare key resolving with
no /
.
A FQNS (having or resolving to contain a /
) can take a few shapes, and should be
looked at in comparison to the basic "unqualified" keyword syntax that uses a
single :
and no /
. FQNS "shortcut" keyword syntax uses leading colons
(::
).
::k
is "local" and ::aa/k
is "aliased")These are "expanded" to a full NS as described above. Think of the ::
as
representing a full long name being squeezed. They can contain dots
but I feel dashes are clearer. Note that the latter contains a /
.
:bb.cc.dd/k
)These are fully spelled out, explicit with dots and dashes in meaningful ways
to represent namespaces containing the keys. The dots are a low-level
(filesystem) detail, but represent the hierarchy of parent(s)/child, wherein
aa.bb.cc
is aa
as grand-parent, bb
as parent, and cc
as child. An
"explicit" FQNS has a single :
but contains a /
(eg, :aa/bb
,
:cc.dd/ee
).
:k
)This is the basic unqualified key you've known about from day-1.
I find it helpful to have your editor make it obvious which shape you're
observing. Here you can see in the screenshot that the ::
is red for the
current NS, the explicit full NS is blue, the "aliased" NS is green, and the
final "key" is purple.
Notice in this example (which you can play with in a REPL) how these "resolve" (see trailing comments). There are more than a dozen cases here that are all a little different.
(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
})
Note that these are all new keys you're "making". The :jj
key doesn't actually
exist in the clojure.string
NS, but that doesn't stop you from using it.
One of the most surprising things is that a /
is added in the expansion in
the local uses (::
). So these three are the ones to focus on remembering:
WRITTEN RESOLVED
:aa => :aa (bare)
::bb => :proj.ns1/bb (qualified)
::ns3/cc => :proj.ns3/cc (qualified, slash explicit)
On the left side:
aa
and bb
look almost identical, but resolve very differentlybb
and cc
look quite different, but they're actually resolving very similarlyI somewhat wish there was ::/foo
instead of ::foo
since the implicit /
in the latter is what makes the system feel irregular.
It's important to know most of these FQNS cases when you're using libraries like malli or spec or integrant or various others, since they make liberal use of FQNSs. And for any sizeable project, it usually becomes necessary to qualify some keys to avoid collisions.
Note the use of :keys
and selection with :
, ::
, and neither in
destructuring with these.
(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"
This is a tricky area of Clojure syntax to keep straight, yet it is an essential part of everyday code. It helps reduce confusion if you follow NS style guides such as these:
Note that there may be disagreement here around aliasing conventions (str
vs
string
, dots in aliases, etc), but the key is to settle your code base
conventions on your rules and stay consistent.
Upvotes: 0
Reputation: 2541
As now documented for Clojure as well as for ClojureScript, ::
keywords can also be used to resolve namespace aliases. For example, ::foo/bar
will evaluate to :clojure.core/bar
if foo
is an alias of clojure.core
. Reader exception is thrown if foo
does not resolve to a namespace.
Upvotes: 31
Reputation: 13514
The double colon is there to fully qualify keywords with your current namespace. This is intended to avoid name clashes for keywords which are meaningful for different libraries. Without fully qualified keywords you might accidentally overwrite some values in a map and break compatibility with a library.
Upvotes: 94