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 都是对的,但这关于别名,把简短的名字空间解析为长的名字空间,这是另一件事。一般而言,通过别名引用是在读取时间(自动解析的 keywords)或编译时间(编译时的 symbols)解析的,所以我认为改变一个别名并不会破坏任何现有的编译引用,只是将来的这种名字的解析会不同。

在非 repl 上下文中,我认为这种情况很少会发生。在动态 repl 上下文中,这将允许您重新定义别名,后续的读取/编译将使用新的别名。与映射不同,别名仅用于命名空间内部的名称解析,而映射作为 var 引用在运行时范围内的存储。
+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
...