Peeyush Kushwaha
Peeyush Kushwaha

Reputation: 3623

Standard practice for the pattern of updating a field when another one updates

Suppose you have two values x and y such that y should be computed every time x is updated. In Java, everything is an object so I have little choice. The way to do it would be via a class, e.g.

public Data {
   private type x, y;

   Data(type x) {
      this.x = x;
      this.y = null;
   }
   
   void updateX(type2 val) {
      // perform operation to update X
      // perform (expensive) operation to update Y
   }
   
   void getX() {...}
   void getY() {...}
}

In clojure, a direct translation of this might use deftype and defprotocol/definterface. But that's too much for just defining a dependency between two variables. In particular, I have an objection to giving it a name and treating it on par with actual types and protocols.

I know this is what defrecords are for -- when you need to make a class that does not represent an entity in the business domain, but then defrecords are immutable.

Do I have other choices? I had a look at RxClojure because the situation here seems to be "reactive", but that seems to be oriented toward "emit"-ing events rather than e.g. just storing the latest one in an atom.

Upvotes: 1

Views: 97

Answers (2)

leetwinski
leetwinski

Reputation: 17859

you could also use add-watch facility for that. For example:

(defn entangle [x-val computation]
  (let [x (atom x-val)
        y (atom (computation x-val))]
    (add-watch x nil (fn [_ _ old-x new-x]
                       (reset! y (computation new-x))))
    {:x x
     :y y}))

In the above example y value depends on x value through the computation function.

(let [{:keys [x y]} (entangle 10 #(* % %))]
  (println "x:" @x "y:" @y)
  (swap! x inc)
  (println "x:" @x "y:" @y)
  (reset! x 200)
  (println "x:" @x "y:" @y))

;; x: 10 y: 100
;; x: 11 y: 121
;; x: 200 y: 40000
nil

This could be handy in some situations, though generally such tangled code eliminates referential transparency, and i would probably avoid using it.

Upvotes: 1

Jared Smith
Jared Smith

Reputation: 21984

I can't help but think that you're overthinking this.

(def some-var (atom some-value))

(def derived (atom (some-expensive-fn some-var)))

(defn update-derived
  [old-state new-state]
  (new-state)

(defn update-var
  [x]
  (swap! derived 
         update-derived 
         (swap! some-var (fn [old-state] (x)))))

We define some state, some derived state, and a function that takes a new value and updates both.

Edit based on comment

If you want to be able to do this multiple times you can use a factory that returns a closure:

(defn make-stateful-thing-with-derived-value
  [init-x-value compute-y]
  (let [x (atom init-x-value)
        y (atom (compute-y init-x-value))]
    (list x
          y
          (fn [new-x-value]
            (swap! y
                   (fn [old-s] (compute-y new-x-value))
                   (swap! x (fn [old-s] (new-x-value)))))))

The function takes an initial x value and a function to derive y and returns a list with the atoms x and y and a function that accepts a value to update them.

Upvotes: 1

Related Questions