2024 Clojure状态调查!中分享您的想法。

欢迎!请查看关于页面以了解更多有关该功能的信息。

+16
Clojure
重新标记

当在一个ns形式中定义命名空间别名,并且稍后更改该别名所引用的命名空间时,你会得到一个错误

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

我在REPL中评估了这个。之后我将它改成了

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

然后我遇到了这个异常

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

这是一个相当棘手的问题。许多人可能找不到比重新启动REPL更好的解决方案。有经验的Clojure开发者会告诉你要这样做

(ns-unalias *ns* 'xxx)

并且重新评估ns形式,但这只是一个相当笨拙的解决方案。正确的做法是丢弃旧别名并接收新的一个。

你(我假设)仍然需要在它被使用之前重新评估使用该别名的代码,但我认为这相当符合Clojure的心理模型。

我们可以重新定义变量,为什么不能重新定义命名空间别名?

4 个答案

+4

称在当前情况下使用ns-unalias是愚蠢的,这种说法并不准确——它确实解决了你描述的问题,而且这种方式与使用Lisp所提出的价值主张相一致。Clojure试图提供的一项保障是引用的稳定性。变量的行为是一个很好的例子,因为你提到了它作为一个推动案例。当你评估类似于(def x 42)的内容时,会创建一个映射,其中x->#'myns.x是当前命名空间中的根绑定42。然而,如果你接着评估(def x 108),则映射到先前相同的变量实例的映射仍然存在,但新的根绑定被设置为108。尽管重定义,引用仍然保持稳定。别名是对变量上下文的间接引用,维护稳定性的意图是禁止自动重新别名的推动因素之一。相反,Clojure通过ns-unalias提供了一种机制,允许用户选择打破这种保证。

尽管如此,我认为修复错误消息以建议使用ns-unalias是合理的,就像与更改命名空间映射相关的错误建议使用ns-unmap一样。

虽然我认为这所有一切对于变量来说都是完全正确的,但这涉及的是将简短命名空间名称解析为较长名称的别名,这是另一回事。通常,通过别名进行引用是在读取时自动解析的(关键字),或在编译时解析符号,因此我认为更改别名不会破坏现有编译引用,只是意味着将来的名称解析会有不同的结果。

在不涉及REPL(读取-评估-打印-循环)的环境中,这种情况很少发生。在动态REPL环境中,这允许你重新定义别名,随后读取/编译将使用新的别名。与映射不同,别名仅在命名空间内部用于名称解析,而映射作为变量引用的全局运行时存储。
+3

我已经将此问题记录在https://clojure.atlassian.net/browse/CLJ-2727上。这需要对这个问题进行更多思考,但我认为这是可以做到的。

+1

为了文档的完整性,另一个解决方案是使用 clojure.tools.namespace.repl/refresh #_(-all)

+1

我同意允许 ns 别名重定义(并警告我,防止踩到自己的脚)是一个很好的增强功能。

如果建议的增强功能被 Clojure.core 成员接受进入 Jira,
所附补丁将替换别名过程中的错误变为警告。

From 906ddea7ba4c051c10e7ab57473e0bdf0b855e02 Mon Sep 17 00:00:00 2001
From: Timothy Pratley <[email protected]>
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
...