【问题标题】:Clojure: How to apply a function to every value in a nested map and update?Clojure:如何将函数应用于嵌套映射中的每个值并更新?
【发布时间】:2017-02-15 14:49:43
【问题描述】:

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

(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))

但我还是需要指定一级键,不能应用到所有一级和二级键值。

【问题讨论】:

  • 你很亲密。只需添加条件和递归调用:(defn map-kv [f coll] (reduce-kv (fn [m k v] (if (map? v) (assoc m k (map-kv f v)) (assoc m k (f v)))) (empty coll) coll))。但是@alan-thompson 的解决方案绝对更简单/惯用。

标签: dictionary clojure nested


【解决方案1】:

您不妨查看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}}

Porthos3 提出了一个很好的观点。以上将转换映射键以及映射值。如果您只想更改值,您可以使用来自the Tupelo Clojure library(Medley 库has a similar function)的map-vals 函数。

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test)
  (:require
    [tupelo.core :as t]
    [clojure.walk :as walk]))

(dotest
  (let [data-2     {1 2
                    3 4}
        tx-vals-fn (fn [item]
                     (if (map? item)
                       (t/map-vals item #(* 2 %))
                       item))
        result     (walk/postwalk tx-vals-fn data-2)]
    (is= (spyx result) {1 4, 3 8})))

结果:

-------------------------------
   Clojure 1.10.1    Java 13
-------------------------------

Testing tst.demo.core
result => {1 4, 3 8}

Ran 2 tests containing 1 assertions.
0 failures, 0 errors.

【讨论】:

  • 您的样本中的输入数据不正确。我认为您不小心复制了输出。
  • 这也将函数应用于映射键。如果数据是 {1 2, 3 4},它将返回 {2 4, 6 8} 而不是 {1 4, 3 8}。
【解决方案2】:

除了 postwalk,正如 Alan 所提到的,递归探索地图并更新每个键是微不足道的。 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}}}

如果您不想包含非核心函数,以下是地图上使用的 fmap 的代码,来自 clojure source(适用于 defn):

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

【讨论】:

  • 这行不通。您的f/fmap[f m] 的上下文中旅行。
【解决方案3】:

我真的很喜欢幽灵,见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 %)))

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

 (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)

【讨论】:

  • CTRL-f "specter"。是的!还有一个很好的实现步行者的例子。
【解决方案4】:
(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? 的表单。

我们不想对键“使用 f 进行 postwalk”,因此我们检查它是否是地图,如果不是地图则跳过它(返回 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)))

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-15
    相关资源
    最近更新 更多