使用宏绑定getter和setter

dby*_*rne 2 macros clojure getter-setter

我的大部分应用程序状态都存储在一个大型复杂的地图中.出于这个问题的目的,我将使用一个简单的结构:

(def data
  {:a 1
   :b {:c {:d 3}}}) 
Run Code Online (Sandbox Code Playgroud)

我有大量的函数都遵循相同的模式:

(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))))
Run Code Online (Sandbox Code Playgroud)

我从地图中检索一个或多个值,执行一些计算,并使用更新的值创建新地图.这种方法存在两个问题:

  • 我在不同的函数定义中有很多重复的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))))
Run Code Online (Sandbox Code Playgroud)

我现在可以更优雅地定义我的功能:

(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)))
Run Code Online (Sandbox Code Playgroud)

展开后,它将生成一个函数定义,如下所示:

(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))))
Run Code Online (Sandbox Code Playgroud)

关于我的宏的一件事是,程序员my-map对函数体中可以使用的函数参数并不直观.

这是一个很好用的宏,还是我应该完全使用不同的方法(如动态var绑定)?

opt*_*evo 5

你也许可以使用镜头; 然后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}
Run Code Online (Sandbox Code Playgroud)

您可以看到上面的镜头可以根据您正在使用的数据结构调整使用get/set方法(例如get-in/update-in).镜头的真正力量似乎也是你所追求的是你可以组成它们.在同一个例子中,组合函数可以定义如下:

(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))
Run Code Online (Sandbox Code Playgroud)

=>功能现在可用于组合任意镜头,例如:

(-> {: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}}
Run Code Online (Sandbox Code Playgroud)

事实上(在[:new]中)只是一个函数意味着你可以,例如,存储它并以各种方式操纵它.例如,可以遍历嵌套的地图结构并创建镜头功能,这些功能对应于访问嵌套地图中每个级别的值,然后最后将这些功能组合在一起以创建getter/setter api.通过此设置,您的镜头可以自动适应架构中的任何更改.

组合镜头的能力还可以使您轻松与嵌套地图的节点进行交互.例如,如果您要将节点从原子更改为列表,则只需添加一个新镜头即可使用它,如下所示:

(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)}
Run Code Online (Sandbox Code Playgroud)

我强烈建议您查看完整的要点,以了解更多关于镜头可以做什么的例子.