Saturday, 04 February 2017

Know your keywords

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 :foo, ::foo, ::bar/foo, and :bar/foo? 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 str. As a result, on line 6, Clojure will see str/join and resolve the namespace alias and interpret the symbol as clojure.string/join.

Note that Clojure treats the str on its own differently than the str before the slash in str/join. The 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 (ns-refers *ns*). In contrast, str/join is a namespace-qualified symbol, so Clojure looks for a join in the str namespace, which is aliased to clojure.string.

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:

  1. Keywords evaluate to themselves, whereas symbols must resolve to a value.
  2. 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 :foo. 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 :clojure.string/foo. 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 awesome or 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 example namespace, :example/foo, ::example/foo, and ::foo all evaluate to the same thing.

TrackBacks

Comments