在 Clojure 中添加向量的惯用方法是啥?

Posted

技术标签:

【中文标题】在 Clojure 中添加向量的惯用方法是啥?【英文标题】:What is the idiomatic way to prepend to a vector in Clojure?在 Clojure 中添加向量的惯用方法是什么? 【发布时间】:2011-05-04 23:42:29 【问题描述】:

添加到列表很容易:

user=> (conj '(:bar :baz) :foo)
(:foo :bar :baz)

附加到向量很容易:

user=> (conj [:bar :baz] :foo) 
[:bar :baz :foo]

我如何(惯用地)添加到向量,同时取回向量? 这不起作用,因为它返回的是序列,而不是向量:

user=> (cons :foo [:bar :baz])     
(:foo :bar :baz)

这很丑(IMVHO):

user=> (apply vector (cons :foo [:bar :baz])) 
[:foo :bar :baz]

注意:我基本上只想要一个可以附加和前置的数据结构。附加到大列表应该会有很大的性能损失,所以我想到了向量..

【问题讨论】:

我会疏忽没有指出最终的“丑陋”示例可以简化为稍微不那么丑陋的形式:(apply vector :foo [:bar :baz])(只需去掉cons)。但我同意这有点尴尬,除了(vector ...) 解决方案之外,基本上只有concat。要是有一个用于喷溅参数的甜美/漂亮的语法,而不是apply(像~@,但不仅仅是宏)... sigh 【参考方案1】:

向量不是为前置而设计的。你只有 O(n) 前置:

user=> (into [:foo] [:bar :baz])
[:foo :bar :baz]

你想要的很可能是finger tree。

【讨论】:

rrb-vectors 可以在 O(log n) 时间内连接(因此预先添加)。见github.com/clojure/core.rrb-vector 我也推荐 rrb-vectors。我找到了这篇文章,在对 Clojure 邮件列表进行挖掘之后,我发现了另外两个应该替换 data.finger-tree 并添加 ClojureScript 支持的库。 rrb-vector 是一个由 Michał Marczyk 维护的贡献项目,他是 Clojure 社区中受人尊敬的贡献者。并不是说其他​​库的作者不是,只是我——作为一个 Clojure 新手——不认识它们。 Clojure 的 Vector 不能以与追加相同的时间复杂度进行前置,这并没有真正的原因。我已经在 Go 中实现了这个,这里:github.com/d11wtq/persistent/tree/feature/prepend。解决方案是 1)记住“起始偏移量”(默认 = 0)并在所有树操作中使用它。 2) 前置时,如果“起始偏移量”=0,分配一个新的根节点,将旧根节点定位在其中一半,并将“起始偏移量”更新到该位置。 3)在所有其他情况下,前置然后只是减少“开始偏移”并在索引0处执行正常关联的情况。 只有在节点将存储数据时才延迟初始化节点,从而消除了该设计中对内存浪费的担忧。 shift 操作(左侧弹出)是通过增加“起始偏移量”并在第零个元素上执行 null 的关联来实现的。必须注意删除索引所遵循的节点路径 我还没有研究过,但我怀疑拆分和连接可以在 O(log32(n)) 中完成,使用相同的数据结构操作实际上是 O(1)。【参考方案2】:

我知道这个问题很老了,但没有人说任何关于差异的事情 列表,既然你说你真的只是想要一些你可以附加的东西 并在前面加上,听起来差异列表可能会对您有所帮助。 它们在 Clojure 中似乎并不流行,但它们非常简单 实现并且比手指树简单得多,所以我做了一个 微小的差异列表库,刚刚(甚至测试过)。这些 在 O(1) 时间内连接(前置或附加)。转换差异 列表返回列表应该花费你 O(n),这是一个很好的权衡,如果 你正在做很多连接。如果你没有做很多 连接,然后只坚持列表,对吗? :)

这是这个小库中的函数:

dl: 差异列表实际上是一个连接自己的函数 带参数的内容并返回结果列表。每次 你产生了一个差异列表,你正在创建一个小函数 就像一个数据结构。

dlempty: 因为差异列表只是将其内容连接到 参数,一个空的差异列表与身份相同 功能。

undl: 由于不同列表的作用,您可以将 只需用 nil 调用它,就可以将差异列表转换为普通列表,所以这个 函数并不是真正需要的;只是为了方便。

dlcons: 将一个项目放在列表的前面——不完全是 必要的,但是 consing 是一个足够常见的操作,它只是一个 单行(就像所有的功能,在这里)。

dlappend: 连接两个差异列表。我认为它的定义是 最有趣的——看看吧! :)

现在,这是一个小型库—— 5 个单行函数,为您提供 O(1) 附加/前置数据结构。还不错吧?啊,拉姆达的美 微积分...

(defn dl
  "Return a difference list for a list"
  [l]
  (fn [x] (concat l x)))

; Return an empty difference list
(def dlempty identity)

(defn undl
  "Return a list for a difference list (just call the difference list with nil)"
  [aDl]
  (aDl nil))

(defn dlcons
  "Cons an item onto a difference list"
  [item aDl]
  (fn [x] (cons item (aDl x))))

(defn dlappend
  "Append two difference lists"
  [dl1 dl2]
  (fn [x] (dl1 (dl2 x))))

您可以通过以下方式看到它的实际效果:

(undl (dlappend (dl '(1 2 3)) (dl '(4 5 6))))

返回:

(1 2 3 4 5 6)

这也返回相同的东西:

((dl '(1 2 3)) '(4 5 6))

享受差异列表的乐趣!

更新

以下是一些可能更难理解但我认为更好的定义:

(defn dl [& elements] (fn [x] (concat elements x)))
(defn dl-un [l] (l nil))
(defn dl-concat [& lists] (fn [x] ((apply comp lists) x)))

这让你可以这样说:

(dl-un (dl-concat (dl 1) (dl 2 3) (dl) (dl 4)))

哪个会返回

(1 2 3 4)

【讨论】:

【参考方案3】:

正如用户 optevo 在手指树下的 cmets 中所说的那样,您可以使用实现 RRB-trees 的 clojure/core.rrb-vector lib:

RRB 树建立在 Clojure 的 PersistentVectors 之上,添加了对数时间连接和切片。除了缺少 vector-of 函数外,相同的 API 支持 ClojureScript。

我决定将此作为单独的答案发布,因为我认为这个库值得。它支持 ClojureScript,由 Michał Marczyk 维护,他在 Clojure 社区中因在实现各种数据结构方面的工作而广为人知。

【讨论】:

【参考方案4】:

如果您不害怕 quasiquoting,这个解决方案实际上非常优雅(对于“优雅”的某些定义):

> `[~:foo ~@[:bar :baz]]

[:foo :bar :baz]

我实际上偶尔会在实际代码中使用它,因为声明式语法使它非常易读,恕我直言。

【讨论】:

确实!我非常羡慕 ES6 扩展语法,但没有意识到我们可以通过准引用来做到这一点: (let [x [1 2]] `[0 ~@x]) => [0 1 2] 【参考方案5】:

我建议使用便利功能built into the Tupelo Library。例如:

(append [1 2] 3  )   ;=> [1 2 3  ]
(append [1 2] 3 4)   ;=> [1 2 3 4]

(prepend   3 [2 1])  ;=> [  3 2 1]
(prepend 4 3 [2 1])  ;=> [4 3 2 1]

相比之下,使用原始 Clojure 很容易出错:

; Add to the end
(concat [1 2] 3)    ;=> IllegalArgumentException
(cons   [1 2] 3)    ;=> IllegalArgumentException
(conj   [1 2] 3)    ;=> [1 2 3]
(conj   [1 2] 3 4)  ;=> [1 2 3 4]

; Add to the beginning
(conj     1 [2 3] ) ;=> ClassCastException
(concat   1 [2 3] ) ;=> IllegalArgumentException
(cons     1 [2 3] ) ;=> (1 2 3)
(cons   1 2 [3 4] ) ;=> ArityException

【讨论】:

以上是关于在 Clojure 中添加向量的惯用方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

“foo = bar || baz”的惯用 Clojure 是啥?

在向量中交换两个元素的惯用方法是啥

在 Clojure 中将嵌套向量减少为另一个向量

从clojure中的普通lisp替换(null x)函数的惯用方法

什么是惯用的clojure:使用

初始化 Java 对象的 Clojure 惯用方法