【发布时间】:2014-09-06 00:32:27
【问题描述】:
我的大部分应用程序状态都存储在一个大型复杂地图中。出于这个问题的目的,我将使用一个简单的结构:
(def data
{:a 1
:b {:c {:d 3}}})
我有大量的函数都遵循相同的模式:
(defn update-map
[my-map val]
(let [a (:a my-map)
d (-> my-map :b :c :d)]
(assoc-in
(assoc my-map :a (+ a val))
[:b :c :d] (+ d val))))
我从地图中检索一个或多个值,执行一些计算,并使用更新的值创建一个新地图。这种方法有两个问题:
- 我在不同的函数定义中有很多重复的 let 绑定
- 如果地图的架构发生变化,我会有很多代码要重构
我编写了一个宏来减少定义这些函数所需的样板代码。它通过查找预定义的 getter 和 setter 函数来工作,并自动生成一个 let 块:
(def getters
{'a #(:a %)
'd #(-> % :b :c :d)})
(def setters
{'a #(assoc % :a %2)
'd #(assoc-in % [:b :c :d] %2)})
(defmacro def-map-fn
[name [& args] [& fields] & code]
(let [my-map 'my-map
lookup #(reduce % [] fields)
getter-funcs (lookup #(conj % %2 (list (getters %2) my-map)))
setter-funcs (lookup #(conj % (symbol (str "update-" %2)) (setters %2)))]
`(defn ~name [~my-map ~@args]
(let [~@getter-funcs ~@setter-funcs]
~@code))))
我现在可以更优雅地定义我的函数:
(def-map-fn update-map
[val] ; normal function parameters
[a d] ; fields from the map I will be using
(update-d
(update-a my-map (+ a val))
(+ d val)))
当展开时,它会产生一个看起来像这样的函数定义:
(defn update-map
[my-map val]
(let [a (#(:a %) my-map)
d (#(-> % :b :c :d) my-map)
update-a #(assoc % :a %2)
update-d #(assoc-in % [:b :c :d] %2)]
(update-d
(update-a my-map (+ a val))
(+ d val))))
关于我的宏让我烦恼的一件事是,对于程序员来说,my-map 函数参数可用于在函数体中使用并不直观。
这是很好地使用宏,还是我应该完全使用不同的方法(如动态 var 绑定)?
【问题讨论】:
-
只是一个简短的评论 - 考虑使用
(update-in my-map [:b :c :d] + val)而不是assoc-in。这至少可以让您避免let绑定。 -
@Alex 我同意你的观点,在这个例子中
update-in比assoc-in更优雅。但是,如果我使用宏来自动检索 setter 函数,我认为我需要在所有字段中一致地使用 assoc 样式,因为我不能总是将新值视为旧值的函数。 -
明白。在这种特殊情况下,我个人的偏好是避免使用宏,因为它们不可组合。例如,当您想在
def-map-fn定义的函数之外使用 getter 或 setter 函数时会发生什么? -
@Alex 绝对是一个有效的观点。那你会采取什么方法呢?我只是不喜欢一直写重复的 let 绑定。动态 var 绑定似乎是一种选择,但这不是很实用。
-
(fn [x] (-> data (update-in [:a] + x) (update-in [:b :c :d] + x)))你能给我们举个例子,说明使用get-in、update-in和assoc-in不能解决问题吗?注意,重新评论,(update-in [keys] (constantly value))与(assoc-in [keys] value)相同。
标签: macros clojure getter-setter