在我自己的数据结构中递增数字时 clojure 中的不变性,从常见的 lisp 可变性到 clojure 不变性
Immutability in clojure when incrementing numbers inside my own data structure, going from common lisp mutability to clojure immutability
在对 lisp 进行更多研究之前,我已经使用了一些 clojure,我想重新学习 clojure...
所以我口齿不清:
(mapcar #'(lambda (x)
(incf (third (car (member x count-mx
:test #'equal
:key #'(lambda (x) (list (car x) (cadr x))))))))
transitions)
对我来说,在 clojure 中翻译成(不确定这是否是好的 clojure):
(map (fn [x] (swap! (third (some #(if (= x (vec (first %) (second %))) %)
count-mx))
inc))
transitions)
其中 count-mx,如果我使用原子来生成零:
[[a a 0] [a b 0] [a c 0] [a d 0] [b a 0] [b b 0] [b c 0] [b d 0] [c a 0] [c b 0] [c c 0] [c d 0] [d a 0] [d b 0] [d c 0] [d d 0]]
和转换:
[[a a] [a b] [b c] [c d] [d a] [a b] [b c]]
objective 在映射过渡并看到 [a a]
后将 [a a 0] 递增到 [a a 1]
尽管 clojure 行确实有效,但由于不变性,我不能在函数或 let 中使用它,我该如何摆脱我常见的 lisp 思维?我非常喜欢在某些东西中映射和修改值,但我不确定如何在 clojure 中正确地有效地做到这一点 and/or。
这就是我在 clojure 中的做法。
user> (def count-mx '[[a a 0] [a b 0] [a c 0] [a d 0] [b a 0] [b b 0] [b c 0] [b d 0] [c a 0] [c b 0] [c c 0] [c d 0] [d a 0] [d b 0] [d c 0] [d d 0]])
#'user/count-mx
user> (def transitions '[[a a] [a b] [b c] [c d] [d a] [a b] [b c]])
#'user/transitions
user> (reduce (fn [cmx xtn]
(mapv (fn [[x1 x2 c :as mx]]
(let [x (pop mx)]
(if (= x xtn)
(conj x (inc c))
mx)))
cmx))
count-mx
transitions)
[[a a 1]
[a b 2]
[a c 0]
[a d 0]
[b a 0]
[b b 0]
[b c 2]
[b d 0]
[c a 0]
[c b 0]
[c c 0]
[c d 1]
[d a 1]
[d b 0]
[d c 0]
[d d 0]]
user>
最好只考虑纯函数,不要担心为了效率而进行变异。比如上面这个例子,count-mx还是原来的那个值。
编辑(改进答案)
我不知道:地图也接受向量作为键。
在这种情况下,我们可以直接将对作为键,并使用 update
结合 inc
来更新值(通过将值增加 1)。我使用 sorted-map
是因为它看起来更适合演示(排序)- 性能更高的方法是在 into
表达式中使用 (hash-map)
或 {}
。
(def m (into (sorted-map) ;; or: `{}` btw `(hash-map)`
(for [x '[a b c d] ;; nesting for-loop
y '[a b c d]] ;; for sunccinct creation
{[x y] 0}))) ;; of all combinations
m
;; => {[a a] 0, [a b] 0, [a c] 0, [a d] 0,
;; [b a] 0, [b b] 0, [b c] 0, [b d] 0,
;; [c a] 0, [c b] 0, [c c] 0, [c d] 0,
;; [d a] 0, [d b] 0, [d c] 0, [d d] 0}
(def transitions '[[a a] [a b] [b c] [c d]
[d a] [a b] [b c]])
(def m' (reduce (fn [a-map a-transition]
(update a-map a-transition inc))
m
transitions))
m'
;; => {[a a] 1, [a b] 2, [a c] 0, [a d] 0,
;; [b a] 0, [b b] 0, [b c] 2, [b d] 0,
;; [c a] 0, [c b] 0, [c c] 0, [c d] 1,
;; [d a] 1, [d b] 0, [d c] 0, [d d] 0}
(reduce (fn [mp key] (update mp key inc)) m '[a b c])
扩展为:
(update (update (update m 'a inc) 'b inc) 'c inc)
因此(以累积的方式)顺序更新 transitions
.
中给出的每个键的值
我们通过以下方式将排序后的映射转换为嵌套向量形式:
(def m'' (into [] (map (comp vec flatten) (seq m'))))
m''
;; => [[a a 1] [a b 2] [a c 0] [a d 0]
;; [b a 0] [b b 0] [b c 2] [b d 0]
;; [c a 0] [c b 0] [c c 0] [c d 1]
;; [d a 1] [d b 0] [d c 0] [d d 0]]
我们可以将它们归纳为函数:
(defn update-map-by-transitions
"Update the map `m` successively by transitions"
[m transitions & {:keys [func] :or {func inc}}]
(reduce (fn [m' tr] (update m' tr func))
m
transitions))
(defn map-to-vecs
"Transform transition map to flatten vectors"
[m & {:keys [vec-func] :or {vec-func flatten}}]
(into [] (map (comp vec vec-func) (seq m))))
(def m' (update-map-by-transitions m transitions))
(def m'' (map-to-vecs m' :vec-func flatten))
m''
;; [[a a 1] [a b 2] [a c 0] [a d 0]
;; [b a 0] [b b 0] [b c 2] [b d 0]
;; [c a 0] [c b 0] [c c 0] [c d 1]
;; [d a 1] [d b 0] [d c 0] [d d 0]]
;; or do:
(def m''' (map-to-vecs m' :vec-func identity))
m'''
;; [[[a a] 1] [[a b] 2] [[a c] 0] [[a d] 0]
;; [[b a] 0] [[b b] 0] [[b c] 2] [[b d] 0]
;; [[c a] 0] [[c b] 0] [[c c] 0] [[c d] 1]
;; [[d a] 1] [[d b] 0] [[d c] 0] [[d d] 0]]
原回答
我也是从 Common Lisp 过来的。顺便说一下,您将其缩写为 CL 或“Lisp”。但 CLISP 实际上意味着 CL 的众多实现之一(仅次于 sbcl、abcl、ECL 等)。所以不要调用 CL clisp ...(以免其他人为此烦恼)。
我认为使用嵌套向量来完成这项任务很乏味,而且 - 当稍后扩展时 - 性能较低。
相反,更喜欢地图。它们具有函数 update
并且在嵌套映射的情况下 update-in
可以操纵和“改变”映射的值,这对于此任务非常方便。
要获取 vec [a b c d]
并生成 {a 0 b 0 c 0 d 0}
,我会定义:
(defn mapper [vec default-val]
(into {} (for [x vec] {x default-val})))
(mapper '[a b] 0) ;; => {a 0 b 0}
现在,我们需要两个键来存储它们的计数值:嵌套映射:
(def m (mapper '[a b c d] (mapper '[a b c d] 0)))
m
;; =>{a {a 0, b 0, c 0, d 0},
;; b {a 0, b 0, c 0, d 0},
;; c {a 0, b 0, c 0, d 0},
;; d {a 0, b 0, c 0, d 0}}
我们可以随时将它们转换回您喜欢的嵌套矢量形式(我承认,它更易于人类阅读):
(defn nested-maps-to-vecs [nested-map]
(vector (for [[k v] nested-map
[ik iv] v]
[k ik iv])))
(nested-maps-to-vecs m)
;;=> [[a a 0] [a b 0] [a c 0] [a d 0]
;; [b a 0] [b b 0] [b c 0] [b d 0]
;; [c a 0] [c b 0] [c c 0] [c d 0]
;; [d a 0] [d b 0] [d c 0] [d d 0]]
现在定义transitions
:
(def transitions '[[a a] [a b] [b c] [c d] [d a] [a b] [b c]])
使用 inc
函数进行更新和方便的 update-in
我们可以
“改变”地图的值:
(def m' (reduce (fn [a-map a-transition] (update-in a-map a-transition inc)) m transitions))
m'
;; => {a {a 1, b 2, c 0, d 0},
;; b {a 0, b 0, c 2, d 0},
;; c {a 0, b 0, c 0, d 1},
;; d {a 1, b 0, c 0, d 0}}
(nested-maps-to-vecs m')
;; => [[a a 1] [a b 2] [a c 0] [a d 0]
;; [b a 0] [b b 0] [b c 2] [b d 0]
;; [c a 0] [c b 0] [c c 0] [c d 1]
;; [d a 1] [d b 0] [d c 0] [d d 0]]
瞧!
(update-in a-map a-transition inc)
得到例如[a b]
并使用键序列 a
访问嵌套映射,然后 b
并获取该“位置”中的值并将 inc
应用于它并“存储”结果进入那个“地方”。
好吧,它实际上并没有存储它,而是 returns 一个具有更新值的新地图。
这个新地图是reduce
函数中的新a-map
,下一个a-transition
这个更新的地图会进一步更新。
所以 reduce
是在遍历 transitions
序列时通过不断更新和捕获更新地图来“累积”更新的技巧。
Clojure 有一种非常有效的方法来“更新”其不可变结构。它仅保存更改,同时引用未更改的其余部分。因此,update
的这种“生成新的更新地图”听起来比实际情况更糟:它实际上是高效的性能 - 以及内存占用方面。由于可变性,Common Lisp 和其他 Lisp 或语言无法采用这种特殊的存储策略。只有像 Clojure 或 Haskell 这样确保数据不可变的纯函数式语言才能使用这种策略来存储和更新其不可变数据。
Clojure 在 atom
中也有可变性,必须明确声明为 atom
。在这种情况下,使用 atom
s 作为地图或嵌套向量中的值并不是 Clojure 的方式。
我还在努力思考这一切。
旅途愉快!
使用 frequencies
来计算转换,然后使用 for
列表理解来构建所需的结果:
(defn mat [transitions]
(let [freqs (frequencies transitions)
verts '[a b c d]]
(for [x verts
y verts]
[x y (get freqs [x y] 0)])))
(def t '[[a a] [a b] [b c] [c d] [d a] [a b] [b c]])
(mat t) ; => ([a a 1] [a b 2] [a c 0] [a d 0]
; [b a 0] [b b 0] [b c 2] [b d 0]
; [c a 0] [c b 0] [c c 0] [c d 1]
; [d a 1] [d b 0] [d c 0] [d d 0])
由于被统计的数据项似乎是像[:a :c]
这样的对,所以我会使用对向量作为映射键。此外,在 Clojure 中,更常见的是使用关键字 :a
作为数据标记而不是符号 a
.
首先定义标记,以及标记对:
(ns tst.demo.core
(:use tupelo.core tupelo.test))
(def tokens [:a :b :c :d])
(def pairs (vec (for [x tokens
y tokens]
[x y])))
然后用每对的零总数初始化计数图:
(def counts (into (sorted-map) (zipmap pairs (repeat 0))))
(dotest
(is= pairs [[:a :a] [:a :b] [:a :c] [:a :d]
[:b :a] [:b :b] [:b :c] [:b :d]
[:c :a] [:c :b] [:c :c] [:c :d]
[:d :a] [:d :b] [:d :c] [:d :d]])
(is= counts
{[:a :a] 0, [:a :b] 0, [:a :c] 0, [:a :d] 0,
[:b :a] 0, [:b :b] 0, [:b :c] 0, [:b :d] 0,
[:c :a] 0, [:c :b] 0, [:c :c] 0, [:c :d] 0,
[:d :a] 0, [:d :b] 0, [:d :c] 0, [:d :d] 0}))
单元测试表明一切如我们所愿。
然后使用 update-in
的简单累加器函数
(defn accumulator
[data]
(let [curr-cnt (atom counts)]
(doseq [item data]
(swap! curr-cnt update-in [item] inc))
(deref curr-cnt)))
现在,定义一些示例数据并验证结果:
(def data [[:a :a] [:a :a] [:a :a] [:a :b] [:b :c] [:c :d] [:d :a] [:a :b] [:b :c]])
(dotest
(is= (accumulator data)
{[:a :a] 3,
[:a :b] 2,
[:a :c] 0,
[:a :d] 0,
[:b :a] 0,
[:b :b] 0,
[:b :c] 2,
[:b :d] 0,
[:c :a] 0,
[:c :b] 0,
[:c :c] 0,
[:c :d] 1,
[:d :a] 1,
[:d :b] 0,
[:d :c] 0,
[:d :d] 0}))
在对 lisp 进行更多研究之前,我已经使用了一些 clojure,我想重新学习 clojure...
所以我口齿不清:
(mapcar #'(lambda (x)
(incf (third (car (member x count-mx
:test #'equal
:key #'(lambda (x) (list (car x) (cadr x))))))))
transitions)
对我来说,在 clojure 中翻译成(不确定这是否是好的 clojure):
(map (fn [x] (swap! (third (some #(if (= x (vec (first %) (second %))) %)
count-mx))
inc))
transitions)
其中 count-mx,如果我使用原子来生成零:
[[a a 0] [a b 0] [a c 0] [a d 0] [b a 0] [b b 0] [b c 0] [b d 0] [c a 0] [c b 0] [c c 0] [c d 0] [d a 0] [d b 0] [d c 0] [d d 0]]
和转换:
[[a a] [a b] [b c] [c d] [d a] [a b] [b c]]
objective 在映射过渡并看到 [a a]
后将 [a a 0] 递增到 [a a 1]尽管 clojure 行确实有效,但由于不变性,我不能在函数或 let 中使用它,我该如何摆脱我常见的 lisp 思维?我非常喜欢在某些东西中映射和修改值,但我不确定如何在 clojure 中正确地有效地做到这一点 and/or。
这就是我在 clojure 中的做法。
user> (def count-mx '[[a a 0] [a b 0] [a c 0] [a d 0] [b a 0] [b b 0] [b c 0] [b d 0] [c a 0] [c b 0] [c c 0] [c d 0] [d a 0] [d b 0] [d c 0] [d d 0]])
#'user/count-mx
user> (def transitions '[[a a] [a b] [b c] [c d] [d a] [a b] [b c]])
#'user/transitions
user> (reduce (fn [cmx xtn]
(mapv (fn [[x1 x2 c :as mx]]
(let [x (pop mx)]
(if (= x xtn)
(conj x (inc c))
mx)))
cmx))
count-mx
transitions)
[[a a 1]
[a b 2]
[a c 0]
[a d 0]
[b a 0]
[b b 0]
[b c 2]
[b d 0]
[c a 0]
[c b 0]
[c c 0]
[c d 1]
[d a 1]
[d b 0]
[d c 0]
[d d 0]]
user>
最好只考虑纯函数,不要担心为了效率而进行变异。比如上面这个例子,count-mx还是原来的那个值。
编辑(改进答案)
我不知道:地图也接受向量作为键。
在这种情况下,我们可以直接将对作为键,并使用 update
结合 inc
来更新值(通过将值增加 1)。我使用 sorted-map
是因为它看起来更适合演示(排序)- 性能更高的方法是在 into
表达式中使用 (hash-map)
或 {}
。
(def m (into (sorted-map) ;; or: `{}` btw `(hash-map)`
(for [x '[a b c d] ;; nesting for-loop
y '[a b c d]] ;; for sunccinct creation
{[x y] 0}))) ;; of all combinations
m
;; => {[a a] 0, [a b] 0, [a c] 0, [a d] 0,
;; [b a] 0, [b b] 0, [b c] 0, [b d] 0,
;; [c a] 0, [c b] 0, [c c] 0, [c d] 0,
;; [d a] 0, [d b] 0, [d c] 0, [d d] 0}
(def transitions '[[a a] [a b] [b c] [c d]
[d a] [a b] [b c]])
(def m' (reduce (fn [a-map a-transition]
(update a-map a-transition inc))
m
transitions))
m'
;; => {[a a] 1, [a b] 2, [a c] 0, [a d] 0,
;; [b a] 0, [b b] 0, [b c] 2, [b d] 0,
;; [c a] 0, [c b] 0, [c c] 0, [c d] 1,
;; [d a] 1, [d b] 0, [d c] 0, [d d] 0}
(reduce (fn [mp key] (update mp key inc)) m '[a b c])
扩展为:
(update (update (update m 'a inc) 'b inc) 'c inc)
因此(以累积的方式)顺序更新 transitions
.
我们通过以下方式将排序后的映射转换为嵌套向量形式:
(def m'' (into [] (map (comp vec flatten) (seq m'))))
m''
;; => [[a a 1] [a b 2] [a c 0] [a d 0]
;; [b a 0] [b b 0] [b c 2] [b d 0]
;; [c a 0] [c b 0] [c c 0] [c d 1]
;; [d a 1] [d b 0] [d c 0] [d d 0]]
我们可以将它们归纳为函数:
(defn update-map-by-transitions
"Update the map `m` successively by transitions"
[m transitions & {:keys [func] :or {func inc}}]
(reduce (fn [m' tr] (update m' tr func))
m
transitions))
(defn map-to-vecs
"Transform transition map to flatten vectors"
[m & {:keys [vec-func] :or {vec-func flatten}}]
(into [] (map (comp vec vec-func) (seq m))))
(def m' (update-map-by-transitions m transitions))
(def m'' (map-to-vecs m' :vec-func flatten))
m''
;; [[a a 1] [a b 2] [a c 0] [a d 0]
;; [b a 0] [b b 0] [b c 2] [b d 0]
;; [c a 0] [c b 0] [c c 0] [c d 1]
;; [d a 1] [d b 0] [d c 0] [d d 0]]
;; or do:
(def m''' (map-to-vecs m' :vec-func identity))
m'''
;; [[[a a] 1] [[a b] 2] [[a c] 0] [[a d] 0]
;; [[b a] 0] [[b b] 0] [[b c] 2] [[b d] 0]
;; [[c a] 0] [[c b] 0] [[c c] 0] [[c d] 1]
;; [[d a] 1] [[d b] 0] [[d c] 0] [[d d] 0]]
原回答
我也是从 Common Lisp 过来的。顺便说一下,您将其缩写为 CL 或“Lisp”。但 CLISP 实际上意味着 CL 的众多实现之一(仅次于 sbcl、abcl、ECL 等)。所以不要调用 CL clisp ...(以免其他人为此烦恼)。
我认为使用嵌套向量来完成这项任务很乏味,而且 - 当稍后扩展时 - 性能较低。
相反,更喜欢地图。它们具有函数 update
并且在嵌套映射的情况下 update-in
可以操纵和“改变”映射的值,这对于此任务非常方便。
要获取 vec [a b c d]
并生成 {a 0 b 0 c 0 d 0}
,我会定义:
(defn mapper [vec default-val]
(into {} (for [x vec] {x default-val})))
(mapper '[a b] 0) ;; => {a 0 b 0}
现在,我们需要两个键来存储它们的计数值:嵌套映射:
(def m (mapper '[a b c d] (mapper '[a b c d] 0)))
m
;; =>{a {a 0, b 0, c 0, d 0},
;; b {a 0, b 0, c 0, d 0},
;; c {a 0, b 0, c 0, d 0},
;; d {a 0, b 0, c 0, d 0}}
我们可以随时将它们转换回您喜欢的嵌套矢量形式(我承认,它更易于人类阅读):
(defn nested-maps-to-vecs [nested-map]
(vector (for [[k v] nested-map
[ik iv] v]
[k ik iv])))
(nested-maps-to-vecs m)
;;=> [[a a 0] [a b 0] [a c 0] [a d 0]
;; [b a 0] [b b 0] [b c 0] [b d 0]
;; [c a 0] [c b 0] [c c 0] [c d 0]
;; [d a 0] [d b 0] [d c 0] [d d 0]]
现在定义transitions
:
(def transitions '[[a a] [a b] [b c] [c d] [d a] [a b] [b c]])
使用 inc
函数进行更新和方便的 update-in
我们可以
“改变”地图的值:
(def m' (reduce (fn [a-map a-transition] (update-in a-map a-transition inc)) m transitions))
m'
;; => {a {a 1, b 2, c 0, d 0},
;; b {a 0, b 0, c 2, d 0},
;; c {a 0, b 0, c 0, d 1},
;; d {a 1, b 0, c 0, d 0}}
(nested-maps-to-vecs m')
;; => [[a a 1] [a b 2] [a c 0] [a d 0]
;; [b a 0] [b b 0] [b c 2] [b d 0]
;; [c a 0] [c b 0] [c c 0] [c d 1]
;; [d a 1] [d b 0] [d c 0] [d d 0]]
瞧!
(update-in a-map a-transition inc)
得到例如[a b]
并使用键序列 a
访问嵌套映射,然后 b
并获取该“位置”中的值并将 inc
应用于它并“存储”结果进入那个“地方”。
好吧,它实际上并没有存储它,而是 returns 一个具有更新值的新地图。
这个新地图是reduce
函数中的新a-map
,下一个a-transition
这个更新的地图会进一步更新。
所以 reduce
是在遍历 transitions
序列时通过不断更新和捕获更新地图来“累积”更新的技巧。
Clojure 有一种非常有效的方法来“更新”其不可变结构。它仅保存更改,同时引用未更改的其余部分。因此,update
的这种“生成新的更新地图”听起来比实际情况更糟:它实际上是高效的性能 - 以及内存占用方面。由于可变性,Common Lisp 和其他 Lisp 或语言无法采用这种特殊的存储策略。只有像 Clojure 或 Haskell 这样确保数据不可变的纯函数式语言才能使用这种策略来存储和更新其不可变数据。
Clojure 在 atom
中也有可变性,必须明确声明为 atom
。在这种情况下,使用 atom
s 作为地图或嵌套向量中的值并不是 Clojure 的方式。
我还在努力思考这一切。
旅途愉快!
使用 frequencies
来计算转换,然后使用 for
列表理解来构建所需的结果:
(defn mat [transitions]
(let [freqs (frequencies transitions)
verts '[a b c d]]
(for [x verts
y verts]
[x y (get freqs [x y] 0)])))
(def t '[[a a] [a b] [b c] [c d] [d a] [a b] [b c]])
(mat t) ; => ([a a 1] [a b 2] [a c 0] [a d 0]
; [b a 0] [b b 0] [b c 2] [b d 0]
; [c a 0] [c b 0] [c c 0] [c d 1]
; [d a 1] [d b 0] [d c 0] [d d 0])
由于被统计的数据项似乎是像[:a :c]
这样的对,所以我会使用对向量作为映射键。此外,在 Clojure 中,更常见的是使用关键字 :a
作为数据标记而不是符号 a
.
首先定义标记,以及标记对:
(ns tst.demo.core
(:use tupelo.core tupelo.test))
(def tokens [:a :b :c :d])
(def pairs (vec (for [x tokens
y tokens]
[x y])))
然后用每对的零总数初始化计数图:
(def counts (into (sorted-map) (zipmap pairs (repeat 0))))
(dotest
(is= pairs [[:a :a] [:a :b] [:a :c] [:a :d]
[:b :a] [:b :b] [:b :c] [:b :d]
[:c :a] [:c :b] [:c :c] [:c :d]
[:d :a] [:d :b] [:d :c] [:d :d]])
(is= counts
{[:a :a] 0, [:a :b] 0, [:a :c] 0, [:a :d] 0,
[:b :a] 0, [:b :b] 0, [:b :c] 0, [:b :d] 0,
[:c :a] 0, [:c :b] 0, [:c :c] 0, [:c :d] 0,
[:d :a] 0, [:d :b] 0, [:d :c] 0, [:d :d] 0}))
单元测试表明一切如我们所愿。
然后使用 update-in
(defn accumulator
[data]
(let [curr-cnt (atom counts)]
(doseq [item data]
(swap! curr-cnt update-in [item] inc))
(deref curr-cnt)))
现在,定义一些示例数据并验证结果:
(def data [[:a :a] [:a :a] [:a :a] [:a :b] [:b :c] [:c :d] [:d :a] [:a :b] [:b :c]])
(dotest
(is= (accumulator data)
{[:a :a] 3,
[:a :b] 2,
[:a :c] 0,
[:a :d] 0,
[:b :a] 0,
[:b :b] 0,
[:b :c] 2,
[:b :d] 0,
[:c :a] 0,
[:c :b] 0,
[:c :c] 0,
[:c :d] 1,
[:d :a] 1,
[:d :b] 0,
[:d :c] 0,
[:d :d] 0}))