当元素通过相同的形式生成时,无法使用字面量语法创建具有唯一成员/键的集合/映射。此类合法形式的例子: (rand), (read), (clojure.core.async/<!!),等等。以下示例使用 (rand)。
user=> #{(rand) (rand)}
IllegalArgumentException 重复键:(rand) clojure.lang.PersistentHashSet.createWithCheck (PersistentHashSet.java:68)
user=> {(rand) 1, (rand) 2}
IllegalArgumentException 重复键:(rand) clojure.lang.PersistentArrayMap.createWithCheck (PersistentArrayMap.java:70)
看起来,在评估集合构造函数的参数之前,已经开始检查重复项。但是,这并不能防止稍后再次进行检查。
注意,仍然会(正确地)检测到重复项(在评估之后),即使在源代码中不会出现字面量形式的重复
user=> #{(+ 1 1) 2}
IllegalArgumentException 重复键:2 clojure.lang.PersistentHashSet.createWithCheck (PersistentHashSet.java:56)
user=> {(+ 1 1) :a, 2 :b}
IllegalArgumentException 重复键:2 clojure.lang.PersistentArrayMap.createWithCheck (PersistentArrayMap.java:70)
因此,第一次重复检查似乎是多余的且不正确的。
请注意,这种急切的重复项检查似乎比语法引号读取宏的优先级更高。
user=> `#{~(rand) ~(rand)}
IllegalArgumentException 重复键:clojure.core/unquote (rand) clojure.lang.PersistentHashSet.createWithCheck (PersistentHashSet.java:68)
user=> `{~(rand) 1, ~(rand) 2}
IllegalArgumentException 重复键:clojure.core/unquote (rand) clojure.lang.PersistentArrayMap.createWithCheck (PersistentArrayMap.java:70)
这很奇怪——因为语法引号在读取时根本不应该实现一个集合。
bq. 对于列表/向量/集合/映射,语法引号建立一个相应数据结构的模板。在模板内部,无限定形式的行为就像递归语法引号一样,但可以使用无引号或不定界无引号将形式免除这种递归引号,在这种情况下,它们将被 treating as expressions and be replaced in the template by their value, or sequence of values, respectively.(《https://clojure.org/reader》)
不说定义,根据显式扩展的语法引号,我原本期望前面的操作可以正确执行。
通过手动替换所需的输入来伪造预期的宏展开,我得到了预期的结果
user=> '`#{~:a ~:b}
(clojure.core/apply clojure.core/hash-set (clojure.core/seq (clojure.core/concat (clojure.core/list :b) (clojure.core/list :a))))
user=> (clojure.core/apply clojure.core/hash-set (clojure.core/seq (clojure.core/concat (clojure.core/list (rand)) (clojure.core/list (rand)))))
#{0.27341896385866227 0.3051522362827035}
user=> '`{~:a 1, ~:b 2}
(clojure.core/apply clojure.core/hash-map (clojure.core/seq (clojure.core/concat (clojure.core/list :a) (clojure.core/list 1) (clojure.core/list :b) (clojure.core/list 2))))
user=> (clojure.core/apply clojure.core/hash-map (clojure.core/seq (clojure.core/concat (clojure.core/list (rand)) (clojure.core/list 1) (clojure.core/list (rand)) (clojure.core/list 2))))
{0.12476921225204185 2, 0.5807961046096718 1}
在我看来,在集合并集的读写宏评估它们的参数之前,有一个多余的重复检查正在运行。这个检查似乎应该被移除。即使这个检查没有捕捉到一些假阳性重复项(它确实这样做了),但在评估后,显然的第二步检查将会捕捉到所有真正的重复项。
尽管如此,这个检查是否应该发生还不清楚。如果我尝试创建含有重复成员/键的集合/映射,我不会得到一个错误。重复项会被静默地移除或替代。
user=> (set (list 1 1))
#{1}
user=> (hash-map 1 2 1 3)
{1 3}
似乎由读取语法构造的文法结构应该保持一致性。
我可以理解,将字面表示视为“构建请求”而不是尝试模拟字面数据对象的打印表示是合理的。从这个角度来看,禁止“不合法”的打印表示似乎是合理的。不幸的是,由于列表在读取时已经进行了求值,因此在字面向量、集合和映射中已评价的项造成了这个理论的破坏。也就是说,这种集合的打印表示并不是一个精确的可读形式,所以读取时的重复检查仍然无法防止打印/读取表示中的看似不一致性。
user=> '#{(+ 1 1) 2}
#{(+ 1 1) 2}
user=> #{(+ 1 1) 2}
IllegalArgumentException 重复键:2 clojure.lang.PersistentHashSet.createWithCheck (PersistentHashSet.java:56)
鉴于问题无法完全避免,最简单、最一致的方法是将读取器字面构造函数视为它们的运行时对应物,就像语法引用在没有多余的重复检查的情况下所做的那样。