请在 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的行为是一个很好的例子,因为你提到了它作为一个激励案例。当你评估像(def x 42)这样的操作时,会在当前命名空间中创建一个从x->#'myns.x的映射,其根绑定为42。然而,如果你接着评估(def x 108),则与之前相同的Var实例的映射仍然保留,但具有新的根绑定108。即使进行了重新定义,引用仍然保持稳定。别名是Var上下文的一个间接引用,保持稳定性的意图是禁止自动别名重定义的动机。相反,Clojure提供了一个机制,通过ns-unalias允许用户选择打破这一保证。

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

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

在非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
...