Macros 用宏绑定getter和setter
我的大部分应用程序状态存储在一个大型复杂映射中。就本问题而言,我将使用一个简单的结构:Macros 用宏绑定getter和setter,macros,clojure,getter-setter,Macros,Clojure,Getter Setter,我的大部分应用程序状态存储在一个大型复杂映射中。就本问题而言,我将使用一个简单的结构: (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
(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绑定
- 如果映射的模式改变,我将有很多代码需要重构
(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绑定)?您可以使用lents;然后,getter和setter成为可组合函数。看一看或看一看 按照第一个链接,您可以按如下方式设置镜头:
; We only need three fns that know the structure of a lens.
(defn lens [focus fmap] {:focus focus :fmap fmap})
(defn view [x {:keys [focus]}] (focus x))
(defn update [x {:keys [fmap]} f] (fmap f x))
; The identity lens.
(defn fapply [f x] (f x))
(def id (lens identity fapply))
; Setting can be easily defined in terms of update.
(defn put [x l value] (update x l (constantly value)))
(-> 3 (view id))
; 3
(-> 3 (update id inc))
; 4
(-> 3 (put id 7))
; 7
; in makes it easy to define lenses based on paths.
(defn in [path]
(lens
(fn [x] (get-in x path))
(fn [f x] (update-in x path f))))
(-> {:value 3} (view (in [:value])))
; 3
(-> {:value 3} (update (in [:value]) inc))
; {:value 4}
(-> {:value 3} (put (in [:value]) 7))
; {:value 7}
(defn combine [outer inner]
(lens
(fn [x] (-> x (view outer) (view inner)))
(fn [f x] (update x outer #(update % inner f)))))
(defn => [& lenses] (reduce combine lenses))
(def each (lens seq map))
(-> {:values [3 4 5]} (view (=> (in [:values]) each)))
; (3 4 5)
(-> {:values [3 4 5]} (update (=> (in [:values]) each) inc))
; {:values (4 5 6)}
(-> {:values [3 4 5]} (put (=> (in [:values]) each) 7))
; {:values (7 7 7)}
您可以从上面的表格中看到,根据您使用的数据结构,镜头可以调整为使用get/set方法(例如get-in/update-in)。镜头的真正力量似乎也是你所追求的,那就是你可以合成它们。在同一示例中,合成函数可定义如下:
; We only need three fns that know the structure of a lens.
(defn lens [focus fmap] {:focus focus :fmap fmap})
(defn view [x {:keys [focus]}] (focus x))
(defn update [x {:keys [fmap]} f] (fmap f x))
; The identity lens.
(defn fapply [f x] (f x))
(def id (lens identity fapply))
; Setting can be easily defined in terms of update.
(defn put [x l value] (update x l (constantly value)))
(-> 3 (view id))
; 3
(-> 3 (update id inc))
; 4
(-> 3 (put id 7))
; 7
; in makes it easy to define lenses based on paths.
(defn in [path]
(lens
(fn [x] (get-in x path))
(fn [f x] (update-in x path f))))
(-> {:value 3} (view (in [:value])))
; 3
(-> {:value 3} (update (in [:value]) inc))
; {:value 4}
(-> {:value 3} (put (in [:value]) 7))
; {:value 7}
(defn combine [outer inner]
(lens
(fn [x] (-> x (view outer) (view inner)))
(fn [f x] (update x outer #(update % inner f)))))
(defn => [& lenses] (reduce combine lenses))
(def each (lens seq map))
(-> {:values [3 4 5]} (view (=> (in [:values]) each)))
; (3 4 5)
(-> {:values [3 4 5]} (update (=> (in [:values]) each) inc))
; {:values (4 5 6)}
(-> {:values [3 4 5]} (put (=> (in [:values]) each) 7))
; {:values (7 7 7)}
=>函数现在可用于组合任意镜头,例如:
(-> {:new {:value 3}} (view (=> (in [:new]) (in [:value]))))
; 3
(-> {:new {:value 3}} (update (=> (in [:new]) (in [:value])) inc))
; {:new {:value 4}}
(-> {:new {:value 3}} (put (=> (in [:new]) (in [:value])) 7))
; {:new {:value 7}}
(in[:new])只是一个函数这一事实意味着,例如,您可以存储它并以各种方式对其进行操作。例如,可以遍历嵌套贴图结构,创建对应于访问嵌套贴图中每个级别的值的镜头函数,然后在最后将这些函数组合在一起,以创建getter/setter api。通过这种设置,您的镜头可以自动适应模式中的任何更改
合成镜头的功能还可以使与嵌套贴图的节点进行交互变得轻松。例如,如果要将节点从原子更改为列表,只需添加一个新镜头即可,如下所示:
; We only need three fns that know the structure of a lens.
(defn lens [focus fmap] {:focus focus :fmap fmap})
(defn view [x {:keys [focus]}] (focus x))
(defn update [x {:keys [fmap]} f] (fmap f x))
; The identity lens.
(defn fapply [f x] (f x))
(def id (lens identity fapply))
; Setting can be easily defined in terms of update.
(defn put [x l value] (update x l (constantly value)))
(-> 3 (view id))
; 3
(-> 3 (update id inc))
; 4
(-> 3 (put id 7))
; 7
; in makes it easy to define lenses based on paths.
(defn in [path]
(lens
(fn [x] (get-in x path))
(fn [f x] (update-in x path f))))
(-> {:value 3} (view (in [:value])))
; 3
(-> {:value 3} (update (in [:value]) inc))
; {:value 4}
(-> {:value 3} (put (in [:value]) 7))
; {:value 7}
(defn combine [outer inner]
(lens
(fn [x] (-> x (view outer) (view inner)))
(fn [f x] (update x outer #(update % inner f)))))
(defn => [& lenses] (reduce combine lenses))
(def each (lens seq map))
(-> {:values [3 4 5]} (view (=> (in [:values]) each)))
; (3 4 5)
(-> {:values [3 4 5]} (update (=> (in [:values]) each) inc))
; {:values (4 5 6)}
(-> {:values [3 4 5]} (put (=> (in [:values]) each) 7))
; {:values (7 7 7)}
我强烈建议您阅读全文,以了解更多有关镜头功能的示例。在这种情况下,我倾向于避免使用宏。它们经常混淆代码,但更重要的是它们不可组合。这里的理想解决方案是允许您在
defmap fn
中定义的函数之外使用getter和setter函数。我会尽可能多地使用常规函数和数据
首先,您关心的是,如果您的模式发生更改,则必须重写一堆代码。很公平。为了解决这个问题,我将从地图模式的数据表示开始。有关Clojure的全功能架构库,请参见,但目前应遵循以下原则:
(def my-schema
{:a :int
:b {:c {:d :int}}})
由此,您可以计算模式中所有属性的路径:
(defn paths [m]
(mapcat (fn [[k v]]
(conj (if (map? v)
(map (partial apply vector k) (paths v)))
[k]))
m))
(def property-paths
(into {} (for [path (paths my-schema)] [(last path) path])))
现在,要获取或设置属性,您可以查找其路径,并将其与get in
、update in
等结合使用。视情况而定:
(let [d (get-in my-map (property-paths :d))]
;; Do something with d.
)
如果您厌倦了总是调用get in
、assoc in
等,那么您可以非常轻松地生成一系列getter函数:
(doseq [[p path] property-paths]
(eval `(defn ~(symbol (str "get-" (name p)))
[m#] (get-in m# ~path))))
(doseq [[p path] property-paths]
(eval `(defn ~(symbol (str "set-" (name p)))
[m# v#] (assoc-in m# ~path v#))))
(doseq [[p path] property-paths]
(eval `(defn ~(symbol (str "update-" (name p)))
[m# tail#] (apply update-in m# ~path #tail))))
现在,您的get-a
、set-a
、update-a
函数在代码中随处可见,而无需调用某些uber宏为您设置绑定。例如:
(let [a (get-a my-map)]
(-> my-map
(set-a 42)
(update-d + a)))
如果您真的觉得设置上述let
绑定很乏味,您甚至可以编写一个带有属性的宏,该宏接受映射和属性名称列表,并在绑定这些名称的值的上下文中执行主体。但我可能不会费心
这种方法的优点包括:
它是模式驱动的,因此模式在一个中心位置定义,并用于根据需要生成其他代码
它更喜欢纯函数而不是宏,因此代码更易于重用和组合
它是一种增量方法,允许您的应用程序更自然地增长。与其从一个试图预测您可能需要的所有可能功能的uber宏开始,不如从数据和函数开始,并在宏中添加一些内容,以减少出现使用模式时的重复性
为什么不在
中使用更新
(defn update-map [my-map val]
(-> my-map
(update-in [:a] + val)
(update-in [:b :c :d] + val)))
只是一个简短的评论-考虑使用<代码>(在我的MAP[[更新:B:C:D:+VAL])< /C> >代替<代码> AsCOC在中。这至少可以让您避免使用let
绑定。@Alex我同意您的观点,在本例中updatein
比assoc in
更优雅。但是,如果我使用宏自动检索setter函数,我认为我需要在所有字段中一致地使用assoc样式,因为我不能总是将新值视为旧值的函数。在这种情况下,我个人倾向于避免使用宏,因为它们是不可组合的。例如,如果要在由def map fn
定义的函数之外使用getter或setter函数,会发生什么情况?@Alex肯定是一个有效点。那么你会采取什么方法呢?我只是不喜欢总是写重复的let绑定。动态var绑定似乎是一个选项,但它不是很实用。(fn[x](>数据(在[:a]+x中更新)(在[:b:c:d]+x中更新))
您能给我们展示一个使用进入
,upda的问题示例吗