#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!