Search code examples
clojuredeftype

Cleanly updating the fields of a deftype'd class


I'm using deftype for the first time because I'm writing a priority queue, and defrecord is interfering with implementing ISeq.

To avoid needing to "deconstruct" the class, alter a field, and "reconstruct" it via explicit calls to it's constructor constantly, I found myself needing to write update-like functions for each field that I need to 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)))

Of course, I could write a function to allow for a syntax closer to update, but the need for this at all seems like a smell.

Is this the typical way to alter non-records? I've extracted as much as I can elsewhere, so the number of times that I need to actually alter the queue itself is limited to a few places, but it still feels bulky. Is the only clean solution to write a function/macro similar to Scala's copy for case classes?


Solution

  • i would propose to make up some macro for that.. i ended up with this one:

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

    it just processes arg list of the deftype form, and creates the function update-%TypeName%, that has a similar semantics to simple update, using keyword variant of field name, and returns a clone of object, with altered field.

    quick example:

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

    which expands to the following:

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

    and can be used like this:

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

    you could also avoid generating update-%TypeName% function for every type, using protocol (say Reconstruct) and auto implementing it in macro, but this would make you lose the possibility to use varargs, since they are unsupported for protocol functions (e.g. you won't be able to do this: (update-SomeType :data assoc :a 10 :b 20 :c 30) )

    UPDATE

    there is also one way i can think of to avoid the use of macro here at all. Though it is cheaty one (since it uses the metadata of ->Type constructor), and also probably slow (since it also uses reflection). But still it works:

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

    and it can be used like this:

    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]