【问题标题】:How are map keys interpreted in Clojure?Clojure 中如何解释映射键?
【发布时间】:2016-11-18 07:11:01
【问题描述】:

我正在尝试使用从随机函数确定的键创建地图文字:

user=> {(str (rand-int 5)) "hello" (str (rand-int 5)) "goodbye"}                                            
IllegalArgumentException Duplicate key: (str (rand-int 5))  clojure.lang.PersistentArrayMap.createWithCheck (PersistentArrayMap.java:71)

user=> {(str (rand-int 5)) "hello" (str (rand-int 6)) "goodbye"}    
{"4" "hello", "2" "goodbye"}

阅读器似乎将密钥视为未评估的列表。

我在文档中找不到有关此的任何详细信息。有没有人可以帮助我进一步理解这一点?

【问题讨论】:

标签: clojure read-eval-print-loop


【解决方案1】:

浏览 Clojure 编译器的源代码,我发现以下内容:

  1. 有类LispReader,其中包含负责读取映射文字的嵌套类MapReader。它的方法invoke 读取{} 符号之间的Clojure 表单,并通过calling RT.map 方法返回一个映射(Clojure 表单)。

  2. RT.map calls PersistentHashMap.createWithCheck 其中实际检查重复键 is performed。由于我们正在构建 Clojure 表单 的映射,即使有两个相同的表单评估为不同的值(如您的示例中),检查也会触发。

  3. Compiler 类中评估所有 Clojure 表单,特别是在其嵌套类 MapExpr 中评估地图表单。它的方法eval 评估映射的键和值,并再次使用RT.map 构建持久映射。因此,也会针对评估值执行重复键检查,这就是以下代码也会失败的原因:

(let [x :foo y :foo]
  {x :bar y :baz}) ;; throws duplicated key exception

我不确定为什么作者决定在表单映射和值映射上执行重复键检查。可能,这是某种“快速失败策略”:这种实现会在编译阶段及早报告错误(即使可能存在误报),并且此检查不会延迟到运行时。

【讨论】:

  • 回复的赢家!
  • 我想你已经回答了你自己的问题,即为什么在这两个地方都进行检查 - 读者构建了一个地图(可能有重复的键),并且评估的地图也可能有重复的键。
  • 很好的答案。谢谢@OlegTheCat
【解决方案2】:

所有内容都是由读者产生的。这是读者背后的一个主要思想:它将表单作为数据读取,几乎没有解释。读者将未求值的映射交给编译器。

阅读器通过 assoc 或 conj 逐步构建地图来工作。但是,在过去,这种方法会为您的代码产生一个更奇怪的结果:{(str (rand-int 5)) "goodbye"}。也就是说,正常的关联规则将适用:最后添加的键值对获胜。人们遇到了这个问题,所以现在读者在增量添加值之前执行contains? 检查。

本文更详细地讨论了 Lisp 风格的阅读器:http://calculist.org/blog/2012/04/17/homoiconicity-isnt-the-point/

【讨论】:

  • 谢谢。很好的解释。
【解决方案3】:

你是正确的,读者没有评估地图。

记住,评估发生在阅读之后。

来自Clojure reader reference documentation

也就是说,大多数 Clojure 程序都是从文本文件开始的,读者的任务是解析文本并生成编译器将看到的数据结构。这不仅仅是编译器的一个阶段。阅读器和 Clojure 数据表示在许多可能使用 XML 或 JSON 等的相同上下文中都有自己的实用程序。

有人可能会说阅读器的语法是用字符定义的,而 Clojure 语言的语法是用符号、列表、向量、映射等定义的。阅读器由函数 read 表示,它读取下一种形式 (不是字符)从流中返回,并返回由该表单表示的对象。

评估者需要未评估的地图来遍历它并评估其键和值。如果该映射多次与键具有相同的形式,则它是无效的映射文字。

您可以改用hash-map 函数

(hash-map (rand-int 5) "hello"
          (rand-int 5) "goodbye")`.

请注意,生成的映射可能有一个或两个键,具体取决于键是否不同。

如果您想强制执行两个键,请执行某项操作。喜欢

(zipmap (distinct (repeatedly #(rand-int 5)))
        ["hello" "goodbye"])

【讨论】:

  • 谢谢。很好的答案@leon-grapenthin
  • @stuartrexking 谢谢,请重新考虑接受它。您接受的答案依赖于未记录的实现细节,这些细节可能会发生变化,并且不会触及对回答问题至关重要的语言设计方面。
【解决方案4】:

我不知道有关读者问题的答案,但构建此哈希映射的更安全方法是确保键不同。例如:

(let [[k1 k2] (shuffle (range 5))] 
  {k1 "hello" k2 "goodbye"})

【讨论】:

  • 对大 N 效率不高,复杂度为 O(N),应该是 O(1)。
  • @LeonGrapenthin 您的需求从何而来?
  • 效率。如果在 0-N 范围内生成两个不同的随机数具有 O(N) 的线性复杂度,则该算法会为问题引入不必要的开销。
  • @leon 此示例未针对大数进行优化,因为 OP 不需要这样做。这不是不正确的。但是,n 键/值的示例的复杂性是多少?由于游戏中有随机性,我不太确定。 O(1) 怎么样?
  • 上面的例子避免了生成重复并在以后删除它们。有人可能会争辩说,在表达式 (range n) 中需要与 n 个键/值接近或相等的情况下,它会更有效。
猜你喜欢
  • 1970-01-01
  • 2011-10-04
  • 2011-11-04
  • 1970-01-01
  • 1970-01-01
  • 2012-07-02
  • 1970-01-01
  • 2022-07-29
  • 2012-09-07
相关资源
最近更新 更多