关联或更新Clojure列表和惰性序列。

4
如果我有一个向量(def v [1 2 3]),我可以用(assoc v 0 666)替换第一个元素,得到[666 2 3]
但是如果我在向量上进行映射后尝试做同样的事情:
(def v (map inc [1 2 3]))
(assoc v 0 666)

抛出以下异常:

ClassCastException clojure.lang.LazySeq cannot be cast to clojure.lang.Associative

什么是编辑或更新懒惰序列中单个元素的最惯用方法?
我应该使用map-indexed仅更改索引0,还是将懒惰序列实现为向量,然后通过assoc/update进行编辑? 前者的优点在于保持惰性,而后者虽然效率较低,但可能更明显。
我猜对于第一个元素,我也可以使用drop和cons。 还有其他的方法吗?我无法在任何地方找到任何示例。

你真正想做什么呢?你可以使用 map-indexed,并根据位置更改任意值。然后它仍然是延迟的。如果你想要改变懒序列中某个值,你首先需要实现这个序列。要更改位置42,你首先需要到达那个点吗? - Benjamin Podszun
2个回答

5
什么是编辑或更新惰性序列中单个元素的最惯用方式?
没有内置函数可以修改序列/列表中的单个元素,但是map-indexed可能是最接近的。对于列表来说,这不是一种高效的操作。假设您不需要惰性,我会将序列倒入向量中,这就是mapv所做的,即(into [] (map f coll))。根据您如何使用修改后的序列,将其向量化并进行修改可能同样具有良好的性能表现。
您可以编写一个使用map-indexed完成类似操作的函数,并且它是惰性的:
(defn assoc-seq [s i v]
  (map-indexed (fn [j x] (if (= i j) v x)) s))

如果您想一次性懒惰地完成此工作而不进行向量化,则也可以使用传导器:

(sequence
  (comp
    (map inc)
    (map-indexed (fn [j x] (if (= 0 j) 666 x))))
  [1 2 3])

如果你的用例仅需要修改惰性序列中的第一个项目,则可以做一些更简单的事情,同时保持惰性:

(concat [666] (rest s))

关于优化的评论更新:在更新1,000,000个元素的惰性序列中的第500,000个元素时,leetwinski的assoc-at函数要比其他函数快大约8毫秒。因此,如果您想尽可能地提高本质上低效操作的性能,请使用他的答案:

(def big-lazy (range 1e6))

(crit/bench
  (last (assoc-at big-lazy 500000 666)))
Evaluation count : 1080 in 60 samples of 18 calls.
            Execution time mean : 51.567317 ms
    Execution time std-deviation : 4.947684 ms
  Execution time lower quantile : 47.038877 ms ( 2.5%)
  Execution time upper quantile : 65.604790 ms (97.5%)
                  Overhead used : 1.662189 ns

Found 6 outliers in 60 samples (10.0000 %)
  low-severe     4 (6.6667 %)
  low-mild   2 (3.3333 %)
Variance from outliers : 68.6139 % Variance is severely inflated by outliers
=> nil

(crit/bench
  (last (assoc-seq big-lazy 500000 666)))
Evaluation count : 1140 in 60 samples of 19 calls.
            Execution time mean : 59.553335 ms
    Execution time std-deviation : 4.507430 ms
  Execution time lower quantile : 54.450115 ms ( 2.5%)
  Execution time upper quantile : 69.288104 ms (97.5%)
                  Overhead used : 1.662189 ns

Found 4 outliers in 60 samples (6.6667 %)
  low-severe     4 (6.6667 %)
Variance from outliers : 56.7865 % Variance is severely inflated by outliers
=> nil

assoc-at 版本在更新大型惰性序列中的 第一个 项目时比原版本快2-3倍,但与 (last (concat [666] (rest big-lazy))) 一样快。


谢谢!这正是我在寻找的答案:确认没有内置函数和一些替代实现。例如,我不知道mapv,而且如果更新是转换链的一部分,变压器版本也可能很有用。 - mrucci
请注意,如果您有一百万个项目的集合,并且需要修改第一个(或中间的)项目,则在实现时仍需要映射整个集合。因此,这是完全不优化的。 - leetwinski

3
我建议采用类似于这样的通用方式,如果确实需要此功能(我强烈怀疑):
(defn assoc-at [data i item]
  (if (associative? data)
    (assoc data i item)
    (if-not (neg? i)
      (letfn [(assoc-lazy [i data]
                (cond (zero? i) (cons item (rest data))
                      (empty? data) data
                      :else (lazy-seq (cons (first data)
                                            (assoc-lazy (dec i) (rest data))))))]
        (assoc-lazy i data))
      data)))

user> (assoc-at {:a 10} :b 20)
;; {:a 10, :b 20}

user> (assoc-at [1 2 3 4] 3 101)
;; [1 2 3 101]

user> (assoc-at (map inc [1 2 3 4]) 2 123)
;; (2 3 123 5)

另一种方法是使用 split-at

(defn assoc-at [data i item]
  (if (neg? i)
    data
    (let [[l r] (split-at i data)]
      (if (seq r)
        (concat l [item] (rest r))
        data))))

请注意,这两个函数都会短路coll遍历,而映射方法不会。以下是一些快速而简单的基准测试结果:
(defn massoc-at [data i item]
  (if (neg? i)
    data
    (map-indexed (fn [j x] (if (== i j) item x)) data)))

(time (last (assoc-at (range 10000000) 0 1000)))
;;=> "Elapsed time: 747.921032 msecs"
9999999

(time (last (massoc-at (range 10000000) 0 1000)))
;;=> "Elapsed time: 1525.446511 msecs"
9999999

你为什么怀疑它是必需的?我有一个代表CSV文件的惰性序列,我知道我必须始终“纠正”第一行。 如果我不能更改输入的CSV,我还剩下什么? - mrucci
嗯,我会说你在这里的任务只是改变第一个项目。也就是 (cons (do-something (first data)) (rest data))。当然还有其他情况需要更改非关联集合中的项目,但几乎总是基于其值而非索引来更新项目,因此使用简单的 map 来完成是很合理的。 - leetwinski

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