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 的当前命名空间中创建了一个映射 x->#'myns.x,其根绑定为 42。然而,如果你然后 eval (def x 108),那么之前映射到相同 Var 实例的映射仍然存在,但其根绑定变为 108。即使在重新定义的情况下,引用仍然保持稳定。别名是 Var 上下文的一种间接方式,而维持稳定性的意图是不允许自动重新别名的动机因素。相反,Clojure 通过 ns-unalias 提供了一种机制,允许用户选择打破这一保证。

尽管如此,我认为修复错误消息以推荐使用 ns-unalias 是合理的,就像有关更改 ns 映射的错误推荐使用 ns-unmap 一样。

虽然我认为这完全适用于 vars,但这关于短命名空间别名到长命名空间的别名,这是一回事。通常,通过别名进行引用会在读取时(对于自动解析的键)或编译时(编译符号)进行解析,因此我不认为更改别名会破坏任何现有的编译引用,只意味着未来解析此名称的方式不同。

在非 repl 上下文中,我认为这种情况很少会发生。在动态 repl 上下文中,它将允许你重新定义别名,并且随后的读取/编译将使用新的别名。与映射不同,别名仅在命名空间内用于名称解析,而映射作为全局范围内存储 var 引用的运行时存储。
+3

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

+1
by

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

+1
by

我同意允许重新定义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
...