Стандартная практика для шаблона обновления поля при обновлении другого

#clojure

#clojure

Вопрос:

Предположим, у вас есть два значения x и y такие, которые y должны вычисляться при каждом x обновлении. В Java все является объектом, поэтому у меня мало выбора. Способ сделать это — через класс, например

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

В clojure прямой перевод этого может использовать deftype и defprotocol / definterface . Но это слишком много для простого определения зависимости между двумя переменными. В частности, у меня есть возражения против присвоения ему имени и обработки его наравне с фактическими типами и протоколами.

Я знаю, что для этого нужны defrecord s — когда вам нужно создать класс, который не представляет сущность в бизнес-домене, но тогда defrecord s являются неизменяемыми.

Есть ли у меня другие варианты? Я посмотрел RxClojure , потому что ситуация здесь кажется «реактивной», но, похоже, она ориентирована на «излучение» событий, а не, например, просто сохранение последнего в атоме.

Ответ №1:

для этого вы также можете использовать функцию add-watch. Например:

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

В приведенном выше примере y значение зависит от x значения через computation функцию.

 (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
 

Это может быть удобно в некоторых ситуациях, хотя обычно такой запутанный код устраняет ссылочную прозрачность, и я бы, вероятно, не стал его использовать.

Комментарии:

1. Это именно то, что я искал. Я согласен, что это может сбивать с толку, но я думаю, что путаницей можно было бы управлять (конечно, не устранять), поместив оба ссылки в объект (как вы делаете при возврате entangle ), и пользователи кода знают, что это особый «реактивный» объект, возможно, даже не пытаясьчтобы разделить ссылки на разные переменные, как вы это делали в let .

Ответ №2:

Я не могу не думать, что вы переоцениваете это.

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

Мы определяем некоторое состояние, некоторое производное состояние и функцию, которая принимает новое значение и обновляет оба.

Редактировать на основе комментария

Если вы хотите иметь возможность делать это несколько раз, вы можете использовать фабрику, которая возвращает закрытие:

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

Функция принимает начальное значение x и функцию для получения y и возвращает список с атомами x и y и функцией, которая принимает значение для их обновления.

Комментарии:

1. Ах. Но в отличие от класса или записи, которые могут создаваться несколько раз, это будет ограничено двумя конкретными переменными в программе. Представьте ту же функциональность, но параметризованную some-var и derived

2. @PeeyushKushwaha Проверьте мой обновленный ответ. Ввел его так, что это не редактор, поэтому скобки могут не сбалансироваться в конце, но вы поняли суть.

3. Спасибо за обновление ответа. Полезно использовать замыкание, чтобы независимые объекты (функция обновления, x, y) могли совместно использовать общее состояние, я об этом не думал.

4. @PeeyushKushwaha Я вижу из вашего профиля, что вы веб-разработчик. Вы можете сделать то же самое в Javascript: (init, f) => { let x = init; let y = f(x); return [x, y, (update) => { x = update; y = f(x); }]}

5. Да. Но я не знал, что могу делать замыкания в clojure!