Clojure:如何将函数应用于嵌套映射中的每个值并更新?

时间:2017-02-15 14:49:44

标签: dictionary clojure nested

假设有一个如下所示的嵌套地图:(仅部分嵌套)

(def mymap {:a 10
        :b {:ba 21, :bb 22 :bc 23}
        :c 30
        :d {:da 41, :db 42}})

如何应用函数,比如说(*%2),并更新此映射中的每个值?这没有指定任何键。结果将如下所示:

{:a 20, 
 :b {:ba 42, :bb 44, :bc 46}, 
 :c 60, 
 :d {:da 82, :db 84}}

到目前为止,我想出了这个自己的功能:

(defn map-kv [f coll] (reduce-kv (fn [m k v] (assoc m k (f v))) (empty coll) coll))

但是我仍然需要指定第一级键,不能应用于所有第一级和第二级键值。

4 个答案:

答案 0 :(得分:6)

您可以查看postwalk功能:https://clojuredocs.org/clojure.walk/postwalk

(def data
   {:a 10
    :b {:ba 21, :bb 22 :bc 23}
    :c 30
    :d {:da 41, :db 42}} )

(defn tx-nums [x]
  (if (number? x)
    (* 2 x)
    x))

(postwalk tx-nums data) => 
  {:a 20, 
   :b {:ba 42, :bb 44, :bc 46}, 
   :c 60, 
   :d {:da 82, :db 84}}

答案 1 :(得分:3)

除了行走之外,正如艾伦提到的那样,递归地探索地图并更新每个键是微不足道的。 Clojure提供了一个名为fmap的函数,它只是将函数应用于映射中的每个值。使用:

在project.clj中,声明此依赖项:

[org.clojure/algo.generic "0.1.2"]

在您的代码中,然后要求:

(require '[clojure.algo.generic.functor :as f :only [fmap]])

然后定义一个递归地遍历地图的函数:

(defn fmap*
  [f m]
  (f/fmap #(if (map? %)
             (fmap* f %)
             (f %))
          m))

(fmap*
   (partial * 2) ;; double every number
   {:a 21 :b {:x 11 :y 22 :z {:p 100 :q 200}}})
=> {:a 42, :b {:x 22, :y 44, :z {:p 200, :q 400}}}

如果您不想包含非核心功能,请参阅clojure source(适用于defn)的地图上使用的fmap代码:

(defn fmap [f m]
  (into (empty m) (for [[k v] m] [k (f v)])))

答案 2 :(得分:2)

我非常喜欢幽灵,请参阅https://github.com/nathanmarz/specter

如果您想要更改前2个级别,则调用transform两次是最简单的

(->> mymap 
     (sp/transform [sp/MAP-VALS map? sp/MAP-VALS number?] #(* 2 %))
     (sp/transform [sp/MAP-VALS number?] #(* 2 %)))

如果您真的想以递归方式替换所有内容,也可以在幽灵中实现walk部分。例如,我想在任意结构中浮动所有数字。首先,我必须定义walker(它也处理vector,seq和sets)。这是通用的,所以我可以重复使用它。

 (defprotocolpath WalkValues)

 (extend-protocolpath WalkValues
                 clojure.lang.IPersistentVector [ALL WalkValues]
                 clojure.lang.IPersistentMap [MAP-VALS WalkValues]
                 clojure.lang.IPersistentSet [ALL WalkValues]
                 clojure.lang.ISeq [ALL WalkValues]
                 Object STAY)

但是一旦我这样做了,我可以实现它

 (sp/transform [sp/WalkValues integer?] float mymap)

或在此示例中

 (sp/transform [sp/WalkValues number?] #(* 2 %) mymap)

答案 3 :(得分:0)

(require '[clojure.walk :as walk])

(defn fmap [f m]
  (into (empty m) (for [[k v] m] [k (f v)])))

(defn map-leaves
  [f form]
  (walk/postwalk (fn [m]
                   (if (map? m)
                     (fmap #(if (map? %) % (f %)) m)
                     m))
                 form))

示例:

(map-leaves
 (partial * 2)
 {:a 10
  :b {:ba 21, :bb 22 :bc 23}
  :c 30
  :d {:da 41, :db 42}})
;; {:a 20, :b {:ba 42, :bb 44, :bc 46}, :c 60, :d {:da 82, :db 84}}

说明:

postwalk 在其实现中调用 walk

(defn postwalk
  [f form]
  (walk (partial postwalk f) f form))

walk 检查表单的类型并将表单(映射)与 coll? 进行匹配,然后将内部(带有 f 的 postwalk)与匹配 map-entry? 的表单进行映射.

我们不想对键进行“postwalk with f”,所以我们检查它是否是一张地图,如果它不是一张地图,则跳过它(返回 m)。 (如果您使用地图作为键,则此逻辑失败。)

postwalk 将我们的 f 作为 walk 传递给 outer。 map-leaves 中的 lambda 跳过在结果映射上调用 outer(又名 f)(查看 coll? 匹配),因为它退出递归。 map inner 已经对地图进行了转换。

(defn walk
  [inner outer form]
  (cond
    (list? form)      (outer (apply list (map inner form)))
    (map-entry? form)
    (outer (MapEntry. (inner (key form)) (inner (val form)) nil))
    (seq? form)       (outer (doall (map inner form)))
    (record? form)    (outer (reduce (fn [r x] (conj r (inner x))) form form))
    (coll? form)      (outer (into (empty form) (map inner form)))
    :else             (outer form)))