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

欢迎!请参阅关于页面以了解有关此工作的更多信息。

+16
Clojure
重新标记

在定义一个命名空间别名后,稍后更改该别名所引用的命名空间时,你会收到一个错误

(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-unmap 一样。

By
虽然我认为对于 vars 来说这一切都是完全正确的,但这与别名用于将短命名空间名解析为长命名空间名是不同的事情。通常,通过别名进行的引用(在读取时自动解析关键字,或在编译时解析符号)被解析,因此我认为更改别名并不会破坏任何现有的编译引用,只是未来对这个名字的解析会有不同的结果。

在非 repl 环境中,我认为这种情况很少会发生。在动态 repl 环境中,这将允许您重新定义别名,后续的读取/编译将使用新别名。与映射不同,别名仅用于命名空间内部的名字解析,而映射则是全局存储 Var 引用的运行时存储。
+3 投票
By

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