在 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 是啥?