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力求提供的一个保证是引用的稳定性。Vars的行为是一个很好的例子,因为你提到了它是动机案例。当你eval类似于(def x 42)的东西时,在当前命名空间中创建了一个映射,即x->#'myns.x,其根绑定为42。然而,如果你接着eval (def x 108),则先前与同一个Var实例的映射仍然保留,但根绑定为108。引用虽然在重新定义后保持稳定。别名是到Var上下文的间接引用,而保持稳定性的意图是禁止自动重新别名的动机因素。相反,Clojure通过ns-unalias提供了一种机制,允许用户选择打破这个保证。

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

虽然我认为对于vars来说这一切都是绝对正确的,但这关乎将短命名空间名称解析为长名称的别名,这是另一回事。通常通过别名进行引用的解析将在读取时通过自动解析的关键字完成,或在编译时用于符号。因此,我认为更改别名不会破坏任何现有编译的引用,只是意味着未来对这个名称的解析将不同。

在非REPL环境中,我认为这种情况很少会发生。在动态REPL环境中,这允许你重新定义别名,随后的读取/编译将使用新的别名。与映射不同,别名仅用于名称解析,而映射作为全局存储变量引用的运行时存储。
+3

我已将其记录在https://clojure.atlassian.net/browse/CLJ-2727。这需要更多的思考,但我认为可以完成。

+1
by

仅为了文档记录,另一种解决方案是使用clojure.tools.namespace.repl/refresh #_(-all)

+1
by

我同意允许重新定义命名空间别名是一个很好的改进,但要给出警告以防我误伤自己。

如果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
...