在 Clojure 中使用 Spectre 删除嵌套值
Removing nested values with Specter in Clojure
假设我有一个这样的 Clojure 映射:
(def mymap {:a [1 2 3] :b {:c [] :d [1 2 3]}})
我想要一个函数 remove-emptys 来生成一个新映射,其中删除了 (:b mymap) 中具有空序列作为值的条目。所以 (remove-emptys mymap) 会给出值:
{:a [1 2 3] :b {:d [1 2 3]}}
有没有一种方法可以使用 Spectre 编写函数来执行此操作?
到目前为止,我还没有找到使用幽灵 filterer
的方法,因为当我测试过滤器时,它们似乎接收每个地图条目两次(一次作为地图条目,一次作为 2 长度向量)并且在这些之间给出不同的结果似乎会引起问题。但是,我们不应该在它们可能出现的任何地方删除空序列,只是将条目映射到它们的值。
不过,我似乎确实得到了一种 clojure.walk
可行的方法,您可能仍然感兴趣。
(ns nested-remove
(:require [com.rpl.specter :as s]
[clojure.walk :refer [postwalk]]))
(defn empty-seq-entry? [entry]
(and (map-entry? entry) (sequential? (val entry)) (empty? (val entry))))
(defn remove-empties [root]
(postwalk #(if (map? %) (into (empty %) (remove empty-seq-entry? %)) %) root))
(remove-empties mymap) ;;=> {:a [1 2 3], :b {:d [1 2 3]}}
虽然我对Spectre不是很熟悉,但是除了postwalk
的解决方法,你还可以使用tupelo.forest
from the Tupelo library来解决。您确实需要将数据重新排列为 Hiccup 或 Enlive 格式,然后很容易识别没有子节点的任何节点:
(ns tst.clj.core
(:use clj.core tupelo.test)
(:require
[tupelo.core :as t]
[tupelo.forest :as tf] ))
(t/refer-tupelo)
(defn hid->enlive [hid]
(tf/hiccup->enlive (tf/hid->hiccup hid)))
(defn empty-kids?
[path]
(let [hid (last path)
result (and (tf/node-hid? hid)
(empty? (grab :kids (tf/hid->tree hid))))]
result))
; delete any nodes without children
(dotest
(tf/with-forest (tf/new-forest)
(let [e0 {:tag :root
:attrs {}
:content [{:tag :a
:attrs {}
:content [1 2 3]}
{:tag :b
:attrs {}
:content [{:tag :c
:attrs {}
:content []}
{:tag :d
:attrs {}
:content [1 2 3]}
]}]}
root-hid (tf/add-tree-enlive e0)
empty-paths (tf/find-paths-with root-hid [:** :*] empty-kids?)
empty-hids (mapv last empty-paths)]
(is= (hid->enlive root-hid) ; This is the original tree structure (Enlive format)
{:tag :root,
:attrs {},
:content
[{:tag :a,
:attrs {},
:content
[{:tag :tupelo.forest/raw, :attrs {}, :content [1]}
{:tag :tupelo.forest/raw, :attrs {}, :content [2]}
{:tag :tupelo.forest/raw, :attrs {}, :content [3]}]}
{:tag :b,
:attrs {},
:content
[{:tag :c, :attrs {}, :content []}
{:tag :d,
:attrs {},
:content
[{:tag :tupelo.forest/raw, :attrs {}, :content [1]}
{:tag :tupelo.forest/raw, :attrs {}, :content [2]}
{:tag :tupelo.forest/raw, :attrs {}, :content [3]}]}]}]})
(apply tf/remove-hid empty-hids) ; remove the nodes with no child nodes
(is= (hid->enlive root-hid) ; this is the result (Enlive format)
{:tag :root,
:attrs {},
:content
[{:tag :a,
:attrs {},
:content
[{:tag :tupelo.forest/raw, :attrs {}, :content [1]}
{:tag :tupelo.forest/raw, :attrs {}, :content [2]}
{:tag :tupelo.forest/raw, :attrs {}, :content [3]}]}
{:tag :b,
:attrs {},
:content
[{:tag :d,
:attrs {},
:content
[{:tag :tupelo.forest/raw, :attrs {}, :content [1]}
{:tag :tupelo.forest/raw, :attrs {}, :content [2]}
{:tag :tupelo.forest/raw, :attrs {}, :content [3]}]}]}]})
(is= (tf/hid->hiccup root-hid) ; same result in Hiccup format
[:root
[:a
[:tupelo.forest/raw 1]
[:tupelo.forest/raw 2]
[:tupelo.forest/raw 3]]
[:b
[:d
[:tupelo.forest/raw 1]
[:tupelo.forest/raw 2]
[:tupelo.forest/raw 3]]]])
)))
我也不知道幽灵,但这在普通的 clojure 中非常简单。
(defn remove-empties [m]
(reduce-kv (fn [acc k v]
(cond (map? v) (let [new-v (remove-empties v)]
(if (seq new-v)
(assoc acc k new-v)
acc))
(empty? v) acc
:else (assoc acc k v)))
(empty m), m))
警告:对于极端嵌套的数据结构,它可能会堆栈溢出。
(update my-map :b (fn [b]
(apply dissoc b
(map key (filter (comp empty? val) b)))))
这是幽灵解决方案:
(ns myns.core
(:require
[com.rpl.specter :as spc]))
(def my-map
{:a [1 2 3]
:b {:c []
:d [1 2 3]}})
(defn my-function
[path data]
(let [pred #(and (vector? %) (empty? %))]
(spc/setval [path spc/MAP-VALS pred] spc/NONE data)))
;; (my-function [:b] my-map) => {:a [1 2 3]
;; :b {:d [1 2 3]}}
以下是使用 Specter 的方法:
(use 'com.rpl.specter)
(setval [:b MAP-VALS empty?] NONE my-map)
=> {:a [1 2 3], :b {:d [1 2 3]}}
在英语中,这表示 "Under :b
, find all the map values that are empty?
. Set them to NONE
, i.e. remove them."
假设我们只需要深入一层而不是像接受的答案那样递归搜索:
(setval [:b MAP-VALS empty?] NONE mymap)
一个完全递归的解决方案,可以在任何级别删除地图中的空值
(def my-complex-map {:a [1] :b {:c [] :d [1 2 3] :e {:f "foo" :g []}}})
; declare recursive path that traverses map values
(declarepath DEEP-MAP-VALS)
(providepath DEEP-MAP-VALS (if-path map? [MAP-VALS DEEP-MAP-VALS] STAY))
(setval [DEEP-MAP-VALS empty?] NONE my-complex-map)
; => {:a [1], :b {:d [1 2 3, :e {:f "foo"}}}}
在 using specter recursively 上引用 wiki。
假设我有一个这样的 Clojure 映射:
(def mymap {:a [1 2 3] :b {:c [] :d [1 2 3]}})
我想要一个函数 remove-emptys 来生成一个新映射,其中删除了 (:b mymap) 中具有空序列作为值的条目。所以 (remove-emptys mymap) 会给出值:
{:a [1 2 3] :b {:d [1 2 3]}}
有没有一种方法可以使用 Spectre 编写函数来执行此操作?
到目前为止,我还没有找到使用幽灵 filterer
的方法,因为当我测试过滤器时,它们似乎接收每个地图条目两次(一次作为地图条目,一次作为 2 长度向量)并且在这些之间给出不同的结果似乎会引起问题。但是,我们不应该在它们可能出现的任何地方删除空序列,只是将条目映射到它们的值。
不过,我似乎确实得到了一种 clojure.walk
可行的方法,您可能仍然感兴趣。
(ns nested-remove
(:require [com.rpl.specter :as s]
[clojure.walk :refer [postwalk]]))
(defn empty-seq-entry? [entry]
(and (map-entry? entry) (sequential? (val entry)) (empty? (val entry))))
(defn remove-empties [root]
(postwalk #(if (map? %) (into (empty %) (remove empty-seq-entry? %)) %) root))
(remove-empties mymap) ;;=> {:a [1 2 3], :b {:d [1 2 3]}}
虽然我对Spectre不是很熟悉,但是除了postwalk
的解决方法,你还可以使用tupelo.forest
from the Tupelo library来解决。您确实需要将数据重新排列为 Hiccup 或 Enlive 格式,然后很容易识别没有子节点的任何节点:
(ns tst.clj.core
(:use clj.core tupelo.test)
(:require
[tupelo.core :as t]
[tupelo.forest :as tf] ))
(t/refer-tupelo)
(defn hid->enlive [hid]
(tf/hiccup->enlive (tf/hid->hiccup hid)))
(defn empty-kids?
[path]
(let [hid (last path)
result (and (tf/node-hid? hid)
(empty? (grab :kids (tf/hid->tree hid))))]
result))
; delete any nodes without children
(dotest
(tf/with-forest (tf/new-forest)
(let [e0 {:tag :root
:attrs {}
:content [{:tag :a
:attrs {}
:content [1 2 3]}
{:tag :b
:attrs {}
:content [{:tag :c
:attrs {}
:content []}
{:tag :d
:attrs {}
:content [1 2 3]}
]}]}
root-hid (tf/add-tree-enlive e0)
empty-paths (tf/find-paths-with root-hid [:** :*] empty-kids?)
empty-hids (mapv last empty-paths)]
(is= (hid->enlive root-hid) ; This is the original tree structure (Enlive format)
{:tag :root,
:attrs {},
:content
[{:tag :a,
:attrs {},
:content
[{:tag :tupelo.forest/raw, :attrs {}, :content [1]}
{:tag :tupelo.forest/raw, :attrs {}, :content [2]}
{:tag :tupelo.forest/raw, :attrs {}, :content [3]}]}
{:tag :b,
:attrs {},
:content
[{:tag :c, :attrs {}, :content []}
{:tag :d,
:attrs {},
:content
[{:tag :tupelo.forest/raw, :attrs {}, :content [1]}
{:tag :tupelo.forest/raw, :attrs {}, :content [2]}
{:tag :tupelo.forest/raw, :attrs {}, :content [3]}]}]}]})
(apply tf/remove-hid empty-hids) ; remove the nodes with no child nodes
(is= (hid->enlive root-hid) ; this is the result (Enlive format)
{:tag :root,
:attrs {},
:content
[{:tag :a,
:attrs {},
:content
[{:tag :tupelo.forest/raw, :attrs {}, :content [1]}
{:tag :tupelo.forest/raw, :attrs {}, :content [2]}
{:tag :tupelo.forest/raw, :attrs {}, :content [3]}]}
{:tag :b,
:attrs {},
:content
[{:tag :d,
:attrs {},
:content
[{:tag :tupelo.forest/raw, :attrs {}, :content [1]}
{:tag :tupelo.forest/raw, :attrs {}, :content [2]}
{:tag :tupelo.forest/raw, :attrs {}, :content [3]}]}]}]})
(is= (tf/hid->hiccup root-hid) ; same result in Hiccup format
[:root
[:a
[:tupelo.forest/raw 1]
[:tupelo.forest/raw 2]
[:tupelo.forest/raw 3]]
[:b
[:d
[:tupelo.forest/raw 1]
[:tupelo.forest/raw 2]
[:tupelo.forest/raw 3]]]])
)))
我也不知道幽灵,但这在普通的 clojure 中非常简单。
(defn remove-empties [m]
(reduce-kv (fn [acc k v]
(cond (map? v) (let [new-v (remove-empties v)]
(if (seq new-v)
(assoc acc k new-v)
acc))
(empty? v) acc
:else (assoc acc k v)))
(empty m), m))
警告:对于极端嵌套的数据结构,它可能会堆栈溢出。
(update my-map :b (fn [b]
(apply dissoc b
(map key (filter (comp empty? val) b)))))
这是幽灵解决方案:
(ns myns.core
(:require
[com.rpl.specter :as spc]))
(def my-map
{:a [1 2 3]
:b {:c []
:d [1 2 3]}})
(defn my-function
[path data]
(let [pred #(and (vector? %) (empty? %))]
(spc/setval [path spc/MAP-VALS pred] spc/NONE data)))
;; (my-function [:b] my-map) => {:a [1 2 3]
;; :b {:d [1 2 3]}}
以下是使用 Specter 的方法:
(use 'com.rpl.specter)
(setval [:b MAP-VALS empty?] NONE my-map)
=> {:a [1 2 3], :b {:d [1 2 3]}}
在英语中,这表示 "Under :b
, find all the map values that are empty?
. Set them to NONE
, i.e. remove them."
假设我们只需要深入一层而不是像接受的答案那样递归搜索:
(setval [:b MAP-VALS empty?] NONE mymap)
一个完全递归的解决方案,可以在任何级别删除地图中的空值
(def my-complex-map {:a [1] :b {:c [] :d [1 2 3] :e {:f "foo" :g []}}})
; declare recursive path that traverses map values
(declarepath DEEP-MAP-VALS)
(providepath DEEP-MAP-VALS (if-path map? [MAP-VALS DEEP-MAP-VALS] STAY))
(setval [DEEP-MAP-VALS empty?] NONE my-complex-map)
; => {:a [1], :b {:d [1 2 3, :e {:f "foo"}}}}
在 using specter recursively 上引用 wiki。