Exploring Clojure (Lisp on the JVM) - Part 4: Not Your Daddy's Namespaces
Wednesday, October 29, 2008
Ok, now I know that
Namespaces are probably not the sexiest topic for a blog post and,
since you're reading this at all, you are probably just curious as to
why someone would possibly want to blog about them! Well,
when I first started looking at namespaces in
Clojure, I probably would have said the same thing. However, as I
played around with them a bit, I found that namespaces provided some
interesting info about how Clojure works "under the
covers". So, in this post, I won't try to cover all of the
namespace functions (that's easy enough to read up on), I'll just try to
go through the "interesting bits" that I discovered while playing.
First of all, Clojure exposes 6 different namespaces through it's
API:
- clojure: All of the basic set of Clojure functionality.
- clojure.inspector: The Clojure graphical (Swing) inspector.
- clojure.parallel: The parallel library wraps the ForkJoin (JSR 166) library for parallel processing.
- clojure.set: Set-based processing functionality.
- clojure.xml: Simple XML processing support.
- clojure.zip: Functional hierarchical zipper, with navigation, editing and enumeration. (see Huet)
user> (resolve 'concat) #=(var clojure/concat) user> (resolve 'do) nilTherefore, if you re-define something that is a special form, you have to specify the namespace as well when using the new definition:
user> (defn do [] (+ 1 2)) #=(var user/do) user> (do) nil user> (user/do) 3When you create a new namespace, it contains mappings for the classnames in java.lang, and nothing else (e.g. - there are no vars yet interned in the namespace):
user> (create-ns 'test) #=(find-ns test) user> (count (ns-map 'test)) 96 user> (count (ns-interns 'test)) 0 user> (doseq x (ns-map 'test) (printf "%s: %s\n" (first x) (frest x))) ProcessBuilder: class java.lang.ProcessBuilder Enum: class java.lang.Enum SuppressWarnings: interface java.lang.SuppressWarnings Throwable: class java.lang.Throwable [snipped output]So, regardless of the namespace that you're in, you're always guaranteed to have certain default Java classes/interfaces available in the namespace. When you create your own vars in the namespace, the names are interned in the namespace (with mappings as well):
user> (in-ns 'test) #=(find-ns test) test> (clojure/defn three [] 3) #=(var test/three) test> (in-ns 'user) #=(find-ns user) user> (count (ns-map 'test)) 97 user> (count (ns-interns 'test)) 1It's interesting to compare the default mappings with the ones that are provide in the "clojure" namespace. So, first, remove the example binding that we did previously in the "test" namespace and create the sequence XOR function that I discussed in my previous post:
user> (ns-unmap 'test 'three)
nil
user> (defn seq-xor [& seqs]
(seq (second
(reduce (fn [[all ret] x]
(if (contains? all x)
[all (disj ret x)]
[(conj all x) (conj ret x)]))
[#{} #{}] (mapcat distinct seqs)))))
#=(var user/seq-xor)
Now, list the mappings that are in clojure but not in the new namespace:user> (doseq x (seq-xor (ns-map 'test) (seq-xor (ns-interns 'clojure) (ns-map 'clojure))) (printf "%s: %s\n" (first x) (frest x))) ClassVisitor: interface clojure.asm.ClassVisitor Modifier: class java.lang.reflect.Modifier DynamicClassLoader: class clojure.lang.DynamicClassLoader Reflector: class clojure.lang.Reflector BlockingQueue: interface java.util.concurrent.BlockingQueue Constructor: class java.lang.reflect.Constructor Method: class clojure.asm.commons.Method IProxy: interface clojure.lang.IProxy PersistentHashMap: class clojure.lang.PersistentHashMap RT: class clojure.lang.RT Opcodes: interface clojure.asm.Opcodes Type: class clojure.asm.Type Writer: class java.io.Writer LinkedBlockingQueue: class java.util.concurrent.LinkedBlockingQueue IPersistentMap: interface clojure.lang.IPersistentMap ClassWriter: class clojure.asm.ClassWriter GeneratorAdapter: class clojure.asm.commons.GeneratorAdapter Array: class java.lang.reflect.ArrayAny Java class/interface that is listed in the namespace's mapping is available for use via Clojure's Java Interop support. So, for example, since "Thread" is included by default in the namespace, we can pause the REPL for 1000 ms by doing:
(. Thread (sleep 1000))And, to add a new Java mapping, one just uses Clojure's "import" function:
user> (in-ns 'test) #=(find-ns test) test> (clojure/import '(javax.swing JFrame)) nil test> (in-ns 'user) #=(find-ns user) user> (ns-resolve 'test 'JFrame) #=javax.swing.JFrameSo, namespaces in Clojure provide a mechanism for mapping not just Clojure var names but Java names too! Quite an interesting approach since it helps to make Java Interop so "natural" in Clojure. Namespaces in Clojure are definitely not your daddy's namespaces! ;-)
Update-2008-10-29: On the Clojure mailing list, Stuart Halloway pointed out the "ns" macro:
"You might also mention that when you actually switch to a namespace using the ns macro, clojure gets referred, giving you a bunch more stuff:"Looking at the doc string for the macro, it looks like "ns" provides a bunch of "convenience" functionality for when you're switching to a namespace:user=> (create-ns 'test) #=(find-ns test) user=> (count (ns-map 'test)) 96 user=> (ns test) nil test=> (ns user) nil user=> (count (ns-map 'test)) 513
"Sets *ns* to the namespace named by name (unevaluated), creating it
if needed. references can be zero or more of: (:refer-clojure ...)
(:require ...) (:use ...) (:import ...) (:load ...) with the syntax
of refer-clojure/require/use/import/load respectively, except the
arguments are unevaluated and need not be quoted. If :refer-clojure
is not used, a default (refer 'clojure) is used. Use of ns is preferred
to individual calls to in-ns/require/use/import:"
(ns foo
(:refer-clojure :exclude [ancestors printf])
(:require (clojure.contrib sql sql.tests))
(:use (my.lib this that))
(:import (java.util Date Timer Random)
(java.sql Connection Statement))
(:load "/mystuff/foo.clj"))
Rich Hickey also provided the following clarifications:
"A quick couple of things:I've updated my blog post to say "var" instead of "symbol".
Your post refers to symbols in a few places where it should say vars. Unlike CL, symbols are just names, with no associated values. The things that are more like symbols from that perspective in Clojure are vars, and it is vars that are interned in namespaces. Two symbols with the same name can be distinct objects. A namespace is not a set of symbols but a set of mappings from symbols to references - either vars or classes.
This separation of concerns is an important part of how Clojure is a Lisp-1 while still supporting defmacro semi-hygienically. The reader reads plain symbols and does no interning. The compiler resolves names (symbols) in the compilation namespace in order to find vars/classes. def interns new vars, and import/refer/use can make new mappings.
ns is the preferred way to define/setup a namespace (think defpackage) - it is more declarative. But it is intended to be used once only. Using in-ns as you did is the right way to change namespaces at the repl - ns is not for that.
I can explain more later, but wanted to clarify these points a bit."

