Clojure关联解构在列表或序列上的数字索引:意料之外的结果

3
Clojure的关联解构允许按数字索引解构向量(也可能是序列列表)。这种模式目前在clojure.org上没有提到,但在《The Joy of Clojure》第二版(Michael Fogus、Chris Houser,2014年5月,第59页)中提到。该书中有一个名为“关联解构”的部分,其中包含了这种方法——错误地将基于索引的解构称为“关联解构”的特例,而在该书中,实际上将其称为“使用映射进行解构”。

无论如何,结果都是出乎意料的(Clojure 1.10.0):

在所有情况下,提取索引0和3处的值。

以下内容符合预期:

(let [{firstthing 0, lastthing 3} [1 2 3 4]] [firstthing lastthing])
;=> [1 4]

(let [{firstthing 0, lastthing 3} (vec '(1 2 3 4))] [firstthing lastthing])
;=> [1 4]

但是在列表中:

(let [{firstthing 0, lastthing 3} '(1 2 3 4)] [firstthing lastthing])
;=> [nil 4]

为什么在位置0有nil
同样地:
(let [{firstthing 0, lastthing 3} (seq '(1 2 3 4))] [firstthing lastthing])
;=> [nil 4]

但另一方面:

(let [{firstthing 0, lastthing 3} (vec (seq '(1 2 3 4)))] [firstthing lastthing])
;=> [1 4]

这里发生了什么?

附录:

(let [{firstthing 0, lastthing 3} { 1 2 3 4 } ] [firstthing lastthing])
;=> [nil 4]

...听起来很合理,因为要关联销毁的地图实际上是{1 2, 3 4}。因此,通过整数键(改变我们脚下表达式的含义)而不是位置进行查找的结果将完全是[nil 4]。是否将任何不是向量的内容先倒入到地图中?

(let [{firstthing 10, lastthing 30} (seq '(10 2 30 4))] [firstthing lastthing])
;=> [2 4]

它确实看起来像是这样的....

(let [{firstthing 10, lastthing 30} (seq '(10 2 30 ))] [firstthing lastthing])
; Execution error (IllegalArgumentException) at user/eval367 (REPL:1).
; No value supplied for key: 30

哦,是的。


3
挖掘实现的确对于确定这种行为的确切源头很有用,但简单来说:列表和序列不是可关联的。(map associative? [{} [] ()]): (true true false) - glts
1
不完全是这样 - 任何序列都会被倒入到 map 中,而非仅限于向量。 - amalloy
2个回答

5

Why is there nil at position 0?

(let [{firstthing 0, lastthing 3} '(1 2 3 4)] [firstthing lastthing])
;=> [nil 4]
如果你查看生成的代码,会看到这个`let`:
user=> (macroexpand '(let [{firstthing 0, lastthing 3} '(1 2 3 4)] [firstthing lastthing]))
(let*
  [map__8012 (quote (1 2 3 4))
   map__8012 (if (clojure.core/seq? map__8012)
               (clojure.lang.PersistentHashMap/create (clojure.core/seq map__8012))
               map__8012)
   firstthing (clojure.core/get map__8012 0)
   lastthing (clojure.core/get map__8012 3)]
  [firstthing lastthing])

你看,对于seq?来说,它会被转换成一个map。所以:

user=> (def map__8012 (quote (1 2 3 4)))
#'user/map__8012
user=> (clojure.core/seq? map__8012)
true
user=> (clojure.lang.PersistentHashMap/create (clojure.core/seq map__8012))
{1 2, 3 4}

因此,您将在键0中获得nil,并在键3中获得4

那肯定就是答案了 :-) - David Tonhofer
根据此更新了我的答案。 - Alan Thompson

1
简短的回答是只有映射和向量是可关联的。列表和序列不可关联。映射解构仅适用于可关联结构:
(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test))

(dotest
  (let [mm {:a 1 :b 2}
        vv [1 2 3]
        ll (list 1 2 3)
        sv (seq vv)
        sl (seq ll) ]
    (spyx (associative? mm))
    (spyx (associative? vv))
    (spyx (associative? ll))
    (spyx (associative? sv))
    (spyx (associative? sl)) ) )

带有结果:
(associative? mm) => true
(associative? vv) => true
(associative? ll) => false
(associative? sv) => false
(associative? sl) => false

Clojure经常采取“垃圾进,垃圾出”的(相当苛刻的)态度,而不是在使用无效参数调用函数时抛出异常。

甚至ClojureDocs.org中有一个警告

我认为函数应该默认更加健壮(即检查参数值/类型),只提供削减版作为clojure.core.raw/get或类似名称。


更新

根据 @cfrick 上面的回答,我们可以看出这种情况的起源。由于序列不是映射,因此在解构之前必须将序列转换为映射。因此,let 假定您提供了一系列键值对,如 [k1 v1 k2 v2 ...],应将其转换为映射,如下所示:

(apply hash-map [k1 v1  k2 v2  ...])

如果这不是你想要的,你应该通过以下方式将序列显式转换为关联数组:
(zipmap (range) <the-seq>)

或者,更简单地说:
(vec <the seq>)

或者,您可以提供Clojure所假定存在的键值序列:
(list 0 1  1 2  2 3  3 4)

请注意,如果您的序列长度为奇数,例如(list 1 2 3),则在从序列转换为映射时会出现错误。

每当我听到“associative”这个词时,我就会想到数学术语:_(a + (b + c)) = ((a + b) + c) == a + b + c_。然后我就记得我们这个行业对于术语的使用很随意。而“关联支持”意味着一个对象实现了关联接口。这包括按键查找和创建一个加入了附加键值对的新对象的能力。 - David Tonhofer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接