【问题标题】:Binding Getters and Setters with a Macro使用宏绑定 Getter 和 Setter
【发布时间】: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-inassoc-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-inupdate-inassoc-in 不能解决问题吗?注意,重新评论,(update-in [keys] (constantly value))(assoc-in [keys] value) 相同。

标签: macros clojure getter-setter


【解决方案1】:

您也许可以使用镜头;然后 getter 和 setter 成为可组合的函数。看看herehere

按照第一个链接,您可以如下设置镜头:

; 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}

您可以从上面看到,镜头可以根据您正在使用的数据结构调整为使用 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))

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

(-> {: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。通过此设置,您的镜头可以自动适应架构中的任何更改。

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

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

我强烈建议您查看完整的 gist 以查看更多关于您可以使用镜头做什么的示例。

【讨论】:

  • 你能把你的答案扩大一些吗? StackOverflow 不赞成“仅链接”的答案。
  • 我选择性地抄袭了要点以显示它与问题的关系。值得查看完整的要点,但可以了解更多使用镜头的方式 - 尤其是在过滤方面。
【解决方案2】:

你为什么不直接使用update-in

(defn update-map [my-map val]
  (-> my-map
      (update-in [:a] + val)
      (update-in [:b :c :d] + val)))

【讨论】:

  • 在我给出的示例中,这是可行的,但在实际代码中,我按照这种模式定义了大约 50 个函数,并且更新对其中许多函数不起作用。
【解决方案3】:

在这种情况下,我的偏好是避免使用宏。它们经常混淆代码,但更重要的是它们不是可组合的。这里的理想解决方案是允许您在def-map-fn 中定义的函数之外使用getter 和setter 函数。我会尽可能坚持使用常规函数和数据。

首先,如果架构发生更改,您会担心必须重写一堆代码。很公平。为了解决这个问题,我将从地图模式的数据表示开始。请参阅 Prismatic schema 了解 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-inupdate-in 等结合使用:

(let [d (get-in my-map (property-paths :d))]
  ;; Do something with d.
  )

如果你厌倦了总是调用get-inassoc-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-aset-aupdate-a 函数,而无需调用一些超级宏来为您设置绑定。例如:

(let [a (get-a my-map)]
  (-> my-map
      (set-a 42)
      (update-d + a)))

如果你真的觉得设置上述let 绑定很乏味,你甚至可以编写一个with-properties 宏,它接受一个映射和一个属性名称列表,并在为这些名称绑定值的上下文中执行主体。但我可能不会打扰。

这种方法的优点包括:

  1. 它是模式驱动的,因此模式在一个中心位置定义,并用于根据需要生成其他代码。
  2. 它更喜欢纯函数而不是宏,因此代码的可重用性和可组合性更高。
  3. 这是一种增量方法,可让您的应用程序更自然地增长。与其从尝试预测您可能需要的所有可能功能的超级宏开始,不如从数据和函数开始,并在您看到使用模式出现时加入宏以减轻一些重复性。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-03-03
    • 1970-01-01
    • 2019-11-26
    • 1970-01-01
    • 1970-01-01
    • 2016-02-19
    • 2011-11-09
    相关资源
    最近更新 更多