Welcome! Please see the About page for a little more info on how this works.

+15 votes
in Clojure by
retagged by

When defining a namespace alias in an ns form, and then later changing the namespace that alias refers to, you get an error:

(ns foo.bar
  (:require [my.xxx :as xxx]))

I evaluate this in my REPL. Later I change it to:

(ns foo.bar
  (:require [other.xxx :as xxx]))

And I get this exception

java.lang.IllegalStateException
Alias xxx already exists in namespace foo.bar, aliasing my.xxx

That's a pretty gnarly sharp edge. Many people won't find a better solution than restarting their REPLs. Experienced Clojure developers will tell you to do

(ns-unalias *ns* 'xxx)

and re-evaluate the ns form, but that's a pretty silly workaround. The right thing to do would be to drop the old alias and pick up the new one.

You'd (I supposed) still have to re-evaluate the code that uses the alias before it gets picked up, but that's fairly consistent with Clojure's mental model IMO.

We can redefine vars, why can't we redefine namespace aliases?

4 Answers

+4 votes
by

It's a mischaracterization to say that use of ns-unalias in this case is silly -- it handles exactly the issue that you describe and does so in a way that's congruent with the value proposition of using a Lisp. A guarantee that Clojure attempts to provide is the stability of references. The behavior of Vars is a good example since you mentioned it as a motivating case. When you eval something like (def x 42) a mapping is created in the current namespace of x->#'myns.x with a root binding of 42. However, if the then eval (def x 108) then the mapping to the same Var instance as before remains in place but with a new root binding of 108. The reference remained stable despite a redefinition. An alias is an indirection to Var context and the intent to maintain stability is a motivating factor for disallowing auto-realiasing. Instead, Clojure provides a mechanism via ns-unalias that allows users to opt-in to breaking that guarantee.

All of that said, I do think that it's reasonable to fix the error message such that it recommends the use of ns-unalias much like the error related to changing ns mappings recommends the use of ns-unmap.

by
While I think this all is totally right for vars, this is about aliases for resolving short namespace names to longer ones, which is a different thing. Generally references via aliases are resolved (with auto-resolved keywords at read time, or for symbols at compile time during compilation), so I don't think changing an alias breaks any existing compiled reference, just means that future resolution of this name resolves differently.

In non-repl contexts, I assume this is rarely going to happen. In dynamic repl contexts, it would allow you to redefine the alias and subsequent read/compile would use the new alias. Unlike the mappings, aliases are used only inside the namespace for name resolution, whereas mappings serve as the runtime-wide store of the var reference.
+3 votes
by

I logged this as https://clojure.atlassian.net/browse/CLJ-2727. It needs a bit more thought, but I think this could be done.

+1 vote
by

Just for the sake of documentation, another workaround is to use clojure.tools.namespace.repl/refresh #_(-all).

+1 vote
by

I agree that allowing ns alias redefintion (with a warning to stop me shooting myself in my foot) would be a good enhancment.

If the proposed enhancement is accepted into Jira by a Clojure.core member,
the attached patch downgrades replacing the alias from an error to a warning.

From 906ddea7ba4c051c10e7ab57473e0bdf0b855e02 Mon Sep 17 00:00:00 2001
From: Timothy Pratley <timothypratley@gmail.com>
Date: Mon, 26 Sep 2022 11:36:26 -0700
Subject: [PATCH] [CLJ-pending] allow ns alias redefinition

Loading a namespace at the REPL previously would fail if an alias was
redefined. This change makes redefinition a warning instead of an error.
The intention is to allow users to change their ns definitions and still
be able to reload their code in the REPL. It does mean that people could
erroneously have duplicate aliases defined, but they will receive a
warning in such cases. Additionally users will need to remember to
reload the entire file if they expect the alias change to have any
effect in the file they are working in, or reload the part that they
would like to use the new alias.

Previous discussion here:
https://ask.clojure.org/index.php/12235/can-alias-already-exists-in-namespace-be-fixed
---
 src/jvm/clojure/lang/Namespace.java | 11 ++++++-----
 test/clojure/test_clojure/repl.clj  | 14 +++++++++++---
 2 files changed, 17 insertions(+), 8 deletions(-)

diff --git a/src/jvm/clojure/lang/Namespace.java b/src/jvm/clojure/lang/Namespace.java
index 35981577..19f29c94 100644
--- a/src/jvm/clojure/lang/Namespace.java
+++ b/src/jvm/clojure/lang/Namespace.java
@@ -242,26 +242,27 @@ public Namespace lookupAlias(Symbol alias){
 	IPersistentMap map = getAliases();
 	return (Namespace) map.valAt(alias);
 }

 public void addAlias(Symbol alias, Namespace ns){
 	if (alias == null || ns == null)
 		throw new NullPointerException("Expecting Symbol + Namespace");
 	IPersistentMap map = getAliases();
-	while(!map.containsKey(alias))
+	Object found = map.valAt(alias);
+	if (found != null && found != ns)
+		RT.errPrintWriter().println("WARNING: Alias " + alias + " already exists in namespace "
+				+ name + ", being replaced by " + ns);
+	while(found != ns)
 		{
 		IPersistentMap newMap = map.assoc(alias, ns);
 		aliases.compareAndSet(map, newMap);
 		map = getAliases();
+		found = map.valAt(alias);
 		}
-	// you can rebind an alias, but only to the initially-aliased namespace.
-	if(!map.valAt(alias).equals(ns))
-		throw new IllegalStateException("Alias " + alias + " already exists in namespace "
-		                                   + name + ", aliasing " + map.valAt(alias));
 }

 public void removeAlias(Symbol alias) {
 	IPersistentMap map = getAliases();
 	while(map.containsKey(alias))
 		{
 		IPersistentMap newMap = map.without(alias);
 		aliases.compareAndSet(map, newMap);
diff --git a/test/clojure/test_clojure/repl.clj b/test/clojure/test_clojure/repl.clj
index c7a0c41b..6e3efea8 100644
--- a/test/clojure/test_clojure/repl.clj
+++ b/test/clojure/test_clojure/repl.clj
@@ -1,12 +1,12 @@
 (ns clojure.test-clojure.repl
   (:use clojure.test
         clojure.repl
-        [clojure.test-helper :only [platform-newlines]]
+        [clojure.test-helper :only [platform-newlines should-print-err-message]]
         clojure.test-clojure.repl.example)
   (:require [clojure.string :as str]))

 (deftest test-doc
   (testing "with namespaces"
     (is (= "clojure.pprint"
            (second (str/split-lines (with-out-str (doc clojure.pprint)))))))
   (testing "with special cases"
@@ -42,20 +42,28 @@
     (is (= [] (apropos "nothing-has-this-name"))))

   (testing "with a symbol"
     (is (some #{'clojure.core/defmacro} (apropos 'defmacro)))
     (is (some #{'clojure.core/defmacro} (apropos 'efmac)))
     (is (= [] (apropos 'nothing-has-this-name)))))


-(defmacro call-ns
+(defmacro call-ns
   "Call ns with a unique namespace name. Return the result of calling ns"
   []  `(ns a#))
-(defmacro call-ns-sym
+(defmacro call-ns-sym
   "Call ns wih a unique namespace name. Return the namespace symbol."
   [] `(do (ns a#) 'a#))

 (deftest test-dynamic-ns
   (testing "a call to ns returns nil"
    (is (= nil (call-ns))))
   (testing "requiring a dynamically created ns should not throw an exception"
     (is (= nil (let [a (call-ns-sym)] (require a))))))
+
+(deftest test-redefine-alias
+  (testing "Alias redefinition is allowed for easier REPL interaction, but raises a warning."
+    (should-print-err-message
+     #"WARNING: Alias set already exists in namespace .*, being replaced by clojure.set\r?\n"
+     (do
+       (require '[clojure.pprint :as set])
+       (require '[clojure.set :as set])))))
--
2.37.3
...