[英]Clojure: How to apply a function to every value in a nested map and update?
假设有一个如下所示的嵌套地图:(仅部分嵌套)
(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))
但是我仍然需要指定一个一级键,并且不能应用于所有一级和二级键值。
您可能希望查看postwalk
功能: https : 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 提出了一个很好的观点。 以上将转换地图键以及地图值。 如果您只想更改值,您可以使用Tupelo Clojure 库中的map-vals
函数(Medley 库有一个类似的函数)。
(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.
除了 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 源(适用于 defn):
(defn fmap [f m]
(into (empty m) (for [[k v] m] [k (f v)])))
我真的很喜欢幽灵,请参阅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(它还处理向量、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)
(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?
匹配coll?
然后将内部(它是带有 f 的 postwalk) map-entry?
与map-entry?
匹配的表单上map-entry?
.
我们不想对键进行“postwalk with f”,所以我们检查它是否是一个地图,如果它不是一个地图,则跳过它(返回 m)。 (如果您使用地图作为键,则此逻辑失败。)
postwalk
通过我们的f
进入walk
作为outer
。 当它退出递归时,map-leaves 中的 lambda 跳过对结果映射(查看coll?
匹配)调用outer
(又名f
)。 地图已经被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)))
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.