Namespace-qualified keywords have existed since the beginning of Clojure, but they have seen relatively little use.
However, Clojure 1.9 will introduce spec, and spec uses this feature quite heavily.
Now that this once-obscure feature is getting some real attention, it has led to a lot of confusion.
Do you know the difference between
If not, I hope you will by the end of this post.
First, a refresher on namespace-qualified symbols
Keywords, like symbols, can be qualified with a namespace. As a Clojure developer, you probably have some experience with namespace-qualified symbols. For example, you may have used something like:
(ns example (:require [clojure.string :as str])) (defn print-items [items] (println (str "The items are: " (str/join ", " items))))
On line 2 of this example, we require the namespace
clojure.string and alias it to
As a result, on line 6, Clojure will see
str/join and resolve the namespace alias and interpret the symbol as
Note that Clojure treats the
str on its own differently than the
str before the slash in
str by itself is looked up within the scope of the current namespace, where it has been referred to
clojure.core/str (you can see all referred symbols by running
str/join is a namespace-qualified symbol, so Clojure looks for a
join in the
str namespace, which is aliased to
However, you do not have to use aliases, you can write the following equivalent code:
(ns example (:require [clojure.string])) (defn print-items [items] (println (str "The items are: " (clojure.string/join ", " items))))
In this snippet, we provide the full namespace and there is no need to look up an alias.
However, had we just used
str/join, Clojure would produce the error: No such namespace: str.
How symbols and keywords are different
In the last section, we saw how Clojure treats namespace-qualified symbols. The fact is that it treats namespace-qualified keywords like symbols, with two major differences:
- Keywords evaluate to themselves, whereas symbols must resolve to a value.
- Only keywords that begin with a double colon participate in namespace auto-resolution.
Let’s examine each of these in turn.
Keywords evaluate to themselves
This means that a Clojure keyword can have any namespace we want, it does not have to exist because it does not need to point to anything.
(ns example (:require [clojure.string :as str])) (prn :foo) ;; :foo (prn :example/foo) ;; :example/foo (prn :clojure.string/foo) ;; :clojure.string/foo (prn :clojure.is.awesome/foo) ;; :clojure.is.awesome/foo (prn :str/foo) ;; :str/foo
In line 4, we see that omitting the namespace gets us what we are used to:
:foo evaluates to
However, as we see in lines 5–7, we can add a namespace, which results in having a keyword with that namespace, regardless of whether that namespace exists or not—the keywords merely evaluate to themselves.
However, note that on line 8
:str/foo does not evaluate to
If we want that feature, we need use a double-colon keyword.
Double-colon keywords: keywords with namespace resolution
Keywords that start with two colons instead of just one participate in namespace resolution, much like symbols:
(ns example (:require [clojure.string :as str])) (prn ::str/foo) ;; :clojure.string/foo (prn ::clojure.string/foo) ;; :clojure.string/foo (prn ::awesome/foo) ;; ERROR: Invalid token: ::awesome/foo (prn ::clojure.is.awesome/foo) ;; ERROR: Invalid token: ::clojure.is.awesome/foo
Lines 4 and 5 above produce the same output.
In the case of line 4, Clojure finds that
str is an alias to
clojure.string, so the result is the namespace-qualified keyword
:clojure.string/foo (note there is only a single colon).
On line 5,
clojure.string itself is a namespace, so no additional resolution is needed and the result is
:clojure.string/foo (again, one colon).
Note that lines 6 and 7 both produce Invalid token errors because neither
clojure.is.awesome are valid namespace names or aliases.
One last thing about double-colon keywords: when you use them without specifying a namespace, they evaluate to a namespace-qualified keyword with the current namespace:
(ns example) (prn :example/foo) ;; :example/foo (prn ::example/foo) ;; :example/foo (prn ::foo) ;; :example/foo
As we can see in lines 3–5, within the
::foo all evaluate to the same thing.