为什么要使用 specter
为了说明为什么要推荐大家使用 specter,先举个实际用到的例子🌰
(def db {:custom {:choices {:current-id "1002"
:datas [{:category_id "1002"
:sample [{:id "4"
:name "不需要试穿"
:default_flag "1"}
{:id "5"
:name "半成品试穿"
:default_flag "0"}]}]}}})
上面的这个多层嵌套的数据结构在开发中是比较常见的,smaple
是服装的试样列表,用户可以切换选择。
于是切换事件的代码是这样的:
;; 切换试样
(kf/reg-event-db
:custom/change-sample
(fn [db [id]]
(let [datas (get-in db [:custom :choices :datas])
cat-id (get-in db [:custom :choices :current-id])
new-datas (map (fn [cat]
(if (= cat-id (:category_id cat))
(let [samples (:sample cat)
new-sample (->> samples
(map (fn [sample]
(if (= "1" (:default_flag sample))
(assoc sample :default_flag "0")
sample)) )
(map (fn [sample]
(if (= id (:id sample))
(assoc sample :default_flag "1")
sample)) ))]
(assoc cat :sample new-sample))
cat)) datas)]
(assoc-in db [:custom :choices :datas] new-datas))))
水平有限,实现方式有些小白... 总之代码一大堆,其实只做了两件事:
- 当点击切换试样时,就把之前选中的试样的
default_flag
标记为0 - 新选中的标记为1。
下面我再用specter实现一下:
;; 切换试样
(kf/reg-event-db
:custom/change-sample
(fn [db [id]]
(let [cat-id (get-in db [:custom :choices :current-id])
cur-cat-path [:custom :choices :datas s/ALL #(= cat-id (:category_id %))]
pre-sel-path (into cur-cat-path
[:sample s/ALL #(= "1" (:default_flag %))])
cur-sel-path (into cur-cat-path
[:sample s/ALL #(= id (:id %))])]
(->> db
(s/transform pre-sel-path #(assoc % :default_flag "0"))
(s/transform cur-sel-path #(assoc % :default_flag "1"))))))
通过对比很容易发现,刚才这种方式对数据的处理更为简单直接,代码易读性更高😄
如何使用
-
引用
[com.rpl.specter :as s]
-
transform
(transform apath transform-fn structure)
使用起来非常简单,以刚才上面的样例举例说明:
① 第一个参数是路径的vector,里边存放的是每层的key
[:custom :choices :datas s/ALL 当前选中的分类 :sample s/ALL 当前选中的试样]
注意的点:
-
s/ALL
它的意思是取当前数组的所有值 - 从ALL获取的vector中找到当前选中的分类,可以使用条件函数:
#(= cat-id (:category_id %))
- 获取当前选中的试样,同理~
② 第二个参数是要执行的转换方法,该方法的参数是根据刚才的路径查找到的数据
比如根据刚才的路径在该方法中的参数得到的就是当前选中的试样
之后就是在方法中对该数据的处理,如#(assoc % :default_flag "1")
③ 第三个参数就是你最外层的那个数据 (刚才的就是db)
如果是从当前的分类开始查找,那么这个参数就是当前选中的分类 current-category
当然对应的路径应改为:[:sample s/ALL 当前选中的试样]
-
setvalue
(setval apath aval structure)
该方法跟
transform
用法比较类似,可以看下刚才的代码用setvalue
的实现方式:;; 切换试样 (kf/reg-event-db :custom/change-sample (fn [db [id]] (let [cat-id (get-in db [:custom :choices :current-id]) cur-cat-path [:custom :choices :datas s/ALL #(= cur-id (:category_id %))] pre-sel-path (into cur-cat-path [:sample s/ALL #(= "1" (:default_flag %)) :default_flag]) cur-sel-path (into cur-cat-path [:sample s/ALL #(= id (:id %)) :default_flag])] (->> db (s/setval pre-sel-path "0") (s/setval cur-sel-path "1")))))
通过对比可以发现:
- 路径比
transform
的多了一个key:default_flag
,也就是说它具体定位到了该对象的这个属性 - 函数调用时直接通过path设置了值,没有使用方法
总结:
如果对求得的值需要通过方法对其处理,建议使用transform;
如果只是单纯的对值进行赋值操作,建议使用setvalue。
-
select
(select apath structure)
返回一个vector,里边是查找到的所有元素
如果未找到,则返回 []
(def sample-path [:custom :choices :datas s/ALL #(= "1002" (:category_id %)) :sample s/ALL #(= "5" (:id %))]) (s/select cur-sel-path db) ;; ->>>> [{:id "5" :name "半成品试穿" :default_flag "0"}]
-
select-first
(select-first apath structure)
返回查询到的第一个元素
注意:如果未找到元素,则返回:nil
-
select-any
(select-any apath structure)
返回查询到的第一个元素
注意:如果未找到元素,则返回:
com.rpl.specter/NONE
-
select-any?
(selected-any? apath structure)
返回一个布尔值 true / false
总结
需要返回一个vector,使用select
需要直接返回一个元素,建议使用select-first
需要返回布尔值,即判断该元素是否存在,使用select-any?
-
nthpath
(nthpath & indices)
指定下标
;; 查询指定下标的元素
(select [(nthpath 2)] [1 2 3])
;; => [3]
;; 注意可以指定多个下标参数
(select [(nthpath 0 0)] [[0 1 2] 2 3])
;; => [0]
-
filterer
(filterer & path)
过滤sequence
;; 普通写法
(s/select [s/ALL even?] (range 10))
;; => [0 2 4 6 8]
;; filterer写法
(s/select-one (s/filterer even?) (range 10))
;; => [0 2 4 6 8]
-
view
(view afn)
获取前面的结果并进行方法处理
;; 找出偶数并加一
(s/select [s/ALL even? (s/view inc)] [1 2 5 6 8])
;; => [3 7 9]
;; 找出偶数先加一再乘以10
(s/select [(s/filterer even?) s/ALL (s/view inc) (s/view #(* 10 %))]
[1 2 5 6 8])
;; => [30 70 90]
-
walker
(walker afn)
;; 找出偶数
(s/select (s/walker #(and (number? %) (even? %)))
'(1 (3 4) 2 (6)))
;; => [4 2 6]
;; 注意(2 (3 4) 5 (6 7))的个数是偶数个,符合条件,所以不再向下寻找
(s/setval (s/walker #(and (counted? %) (even? (count %))))
:double
'(1
(2 (3 4) 5 (6 7))
(8 9)))
;; => (1 :double :double)
;; 注意(2 (3 4) 5)的个数是奇数个,不合符条件,所以继续再向下寻找(3 4)
(s/setval (s/walker #(and (counted? %) (even? (count %))))
:double
'(1
(2 (3 4) 5)
(8 9)))
;; => (1 (2 :double 5) :double)
-
selected?
(selected? & path)
(s/setval [s/ALL
(s/selected? (s/filterer even?)
(s/view count) #(> % 2))
s/FIRST]
"😆😆"
[[6 7 8] [1 2 3] [5 6 7 8 9 10]])
;; => [[6 7 8] [1 2 3] ["😆😆" 6 7 8 9 10]]
-
collect
(collect & paths)
根据给定的路径进行select求值,并把结果放到一个vector中
;; 将第一个元素值更新为它与其他所有偶数的和
(s/transform [(s/collect s/ALL even?) s/FIRST]
(fn [evens first]
(prn evens first) ;; => [4 6] 3
(reduce + first evens))
[3 4 5 6])
;; => [13 4 5 6]
-
collect-one
(collect & paths)
同collect,只不过会把结果单独返回(而不是放到vector中)
;; 将最后一个元素值更新为它与第一个元素的和
(s/transform [(s/collect-one s/FIRST) s/LAST]
(fn [first last]
(prn first last) ;; => 1 3
(+ first last))
[1 2 3])
;; => [1 2 4]
注意:无论是collect,还是collect-one,它们都可以在已有的路径后面多次使用。如果使用了transform方法,那么collected得到的value们会作为参数按照先后顺序传递到tranform的处理方法中,并且根据transform的path最终对应的值会作为最后一个参数传递进去。
;; 多个collect的示例
(s/transform [s/ALL (s/collect-one :rate) (s/collect-one :deduct)
:top s/ALL (s/collect-one :b) :a even?]
(fn [rate deduct b e]
(if b
100
(- (* rate e) deduct)))
[{:rate 2.0
:deduct 20
:top [{:a 0 :b 1} {:a 1} {:a 2} {:a 3}]}
{:rate 3.0
:deduct 10
:top [{:a 0} {:a 1} {:a 2} {:a 3}]}])
;; =>
[{:rate 2
:deduct 20
:top [{:a 100 :b 1} {:a 1} {:a -16} {:a 3}]}
{:rate 3
:deduct 10
:top [{:a -10} {:a 1} {:a -4} {:a 3}]}]
-
其他还有很多方法,可以去官网学习一下。。。