代码之家  ›  专栏  ›  技术社区  ›  Carcigenicate

干净地更新deftype类的字段

  •  3
  • Carcigenicate  · 技术社区  · 7 年前

    我在用 deftype defrecord 妨碍实施 ISeq .

    避免需要 "deconstruct" the class, alter a field, and "reconstruct" it via explicit calls to it's constructor constantly ,我发现自己需要写 update -就像我需要的每个领域的函数 alter :

    (deftype Priority-Queue [root size priority-comparator])
    
    (defn- alter-size [^Priority-Queue queue, f]
      (->Priority-Queue (.root queue) (f (.size queue)) (.priority_comparator queue)))
    
    (defn- alter-root [^Priority-Queue queue, f]
      (->Priority-Queue (f (.root queue)) (.size queue) (.priority_comparator queue)))
    

    当然,我可以编写一个函数来允许语法更接近 更新 ,但这一点的必要性似乎是一种气味。

    这是改变非记录的典型方法吗?我已经在其他地方尽可能多地提取了数据,所以实际需要更改队列本身的次数仅限于几个地方,但仍然感觉很庞大。是编写类似于 Scala's copy for case classes ?

    1 回复  |  直到 7 年前
        1
  •  2
  •   leetwinski    7 年前

    我建议做一些宏… 我最后得到了这个:

    (defmacro attach-updater [deftype-form]
      (let [T (second deftype-form)
            argnames (nth deftype-form 2)
            self (gensym "self")
            k (gensym "k")
            f (gensym "f")
            args (gensym "args")]
        `(do ~deftype-form
             (defn ~(symbol (str "update-" T))
               ^{:tag ~T} [^{:tag ~T} ~self ~k ~f & ~args]
               (new ~T ~@(map (fn [arg]
                                (let [k-arg (keyword arg)]
                                  `(if (= ~k ~k-arg)
                                     (apply ~f (. ~self ~arg) ~args)
                                     (. ~self ~arg))))
                              argnames))))))
    

    它只处理deftype表单的arg列表,并创建函数 update-%TypeName% ,其语义类似于simple update ,使用字段名的关键字变量,并返回对象的克隆,其中字段已更改。

    快速示例:

    (attach-updater
     (deftype MyType [a b c]))
    

    扩展到以下内容:

    (do
      (deftype MyType [a b c])
      (defn update-MyType [self14653 k14654 f14655 & args14656]
        (new
          MyType
          (if (= k14654 :a)
            (apply f14655 (. self14653 a) args14656)
            (. self14653 a))
          (if (= k14654 :b)
            (apply f14655 (. self14653 b) args14656)
            (. self14653 b))
          (if (= k14654 :c)
            (apply f14655 (. self14653 c) args14656)
            (. self14653 c)))))
    

    可以这样使用:

    (-> (MyType. 1 2 3)
        (update-MyType :a inc)
        (update-MyType :b + 10 20 30)
        ((fn [item] [(.a item) (.b item) (.c item)])))
    ;;=> [2 62 3]
    
    (attach-updater
      (deftype SomeType [data]))
    
    (-> (SomeType. {:a 10 :b 20})
        (update-SomeType :data assoc :x 1 :y 2 :z 3)
        (.data))
    ;;=> {:a 10, :b 20, :x 1, :y 2, :z 3}
    

    你也可以避免 更新-%typename% 使用协议(例如 Reconstruct )在宏中自动实现,但这将使您失去使用varargs的可能性,因为协议函数不支持varargs(例如,您将无法执行以下操作: (update-SomeType :data assoc :a 10 :b 20 :c 30) )

    更新

    我还有一种方法可以避免在这里使用宏。尽管它是作弊的(因为它使用了 ->Type 构造器),也可能很慢(因为它也使用反射)。但仍然有效:

    (defn make-updater [T constructor-fn]
      (let [arg-names (-> constructor-fn meta :arglists first)]
        (fn [self k f & args]
          (apply constructor-fn
                 (map (fn [arg-name]
                        (let [v (-> T (.getField (name arg-name)) (.get self))]
                          (if (= (keyword (name arg-name))
                                 k)
                            (apply f v args)
                            v)))
                      arg-names)))))
    

    它可以这样使用:

    user> (deftype TypeX [a b c])
    ;;=> user.TypeX
    
    user> (def upd-typex (make-updater TypeX #'->TypeX))
    ;;=> #'user/upd-typex
    
    user> (-> (TypeX. 1 2 3)
              (upd-typex :a inc)
              (upd-typex :b + 10 20 30)
              (#(vector (.a %) (.b %) (.c %))))
    ;;=> [2 62 3]