Clojure: идиоматическое использование атома и ссылки?

Я разрабатываю немного кода Clojure, который будет ссылаться на карту и увеличивать пару значений ключа на карте. Я думаю, что правильно использую ref, но я не уверен насчет атома. Нужно ли использовать swap! быть более идиоматичным? Я новичок в STM и Clojure, выглядит ли это потокобезопасным/нормальным? Что мне не хватает?

(defn increment-key [ref key]
    (dosync
        (if (= (get @ref key) nil)
            (alter ref assoc key (atom 1))
            (alter ref assoc key (atom (inc @(get @ref key)))))))

(defn -main [& args]
    (def my-map (ref {}))
    (increment-key my-map "yellow")
    (println my-map)
    (increment-key my-map "yellow")
    (println my-map))

Отпечатки

$ lein run
#<Ref@494eaec9: {yellow #<Atom@191410e5: 1>}>
#<Ref@494eaec9: {yellow #<Atom@7461373f: 2>}>

person David Williams    schedule 14.04.2013    source источник
comment
круто, спасибо, выложил туда   -  person David Williams    schedule 14.04.2013


Ответы (1)


Это не очень идиоматично для Clojure: встраивание изменяемых объектов в персистентную структуру данных обычно сводит на нет весь смысл неизменяемых структур данных.

Я бы полностью избегал внутреннего атома и просто имел число, связанное с ключом. Тогда вся структура данных, содержащаяся в my-map, будет неизменной.

Что касается безопасности потоков: это действительно зависит от того, как вы собираетесь его использовать. В этом случае ref кажется излишним, поскольку он действительно необходим только тогда, когда вам нужно координировать транзакции между несколькими ссылками, которых здесь нет. Вероятно, atom достаточно для того, что вы пытаетесь сделать.

Вот как вы можете решить эту проблему:

(defn increment-key [ref key]
  (swap! ref update-in [key] (fn [n] (if n (inc n) 1))))

(def my-map (atom {}))
(increment-key my-map "yellow")
(println my-map)  ;; => {"yellow" 1}
(increment-key my-map "yellow")
(println my-map)  ;; => {"yellow" 2}

EDIT: еще лучше было бы исключить изменчивость ваших функций и определить ключ приращения как чистую функцию.

(defn increment-key [m key]
  (assoc m key (if-let [n (m key)] (inc n) 1)))

(def my-map (atom {}))
(swap! my-map increment-key "yellow")
(println my-map)   ;; => {"yellow" 1}
(swap! my-map increment-key "yellow")
(println my-map)   ;; => {"yellow" 2}
person mikera    schedule 14.04.2013
comment
Я ценю ответ. В последнее время я много думал о clojure и пытаюсь концептуализировать, как бы я сделал что-то, что в императивном мире, кажется, требует состояния. Например, наивный байесовский классификатор. Где-то подсчеты должны быть подсчитаны, а затем мы должны вычислить веса на основе этих подсчетов. Есть ли у вас какие-либо советы о том, как думать/моделировать то, что кажется по своей сути сохраняющим состояние в контексте clojure? Есть ли лучший способ управления состоянием, чем передача изменяющихся ссылок? Я чувствую, что собираюсь понять что-то большее, но немного застрял... - person David Williams; 14.04.2013
comment
Я очень редко передаю ссылки/атомы. Большинство моих функций в Clojure чистые: они принимают чистые значения и возвращают новые измененные значения. Я даже написал целую игру, в которой состояние мира представляет собой единую неизменную структуру данных: github.com/mikera/alchemy . Хитрость заключается в том, чтобы рассматривать функции как нечто, возвращающее обновленную версию старых данных с некоторыми изменениями. - person mikera; 14.04.2013
comment
Подождите, разве (lib/setup game) не мутирует игра, которая является изменяемым PersistentTreeGrid? мир.clj:31 - person David Williams; 14.04.2013
comment
@mikera Кстати, (fn [n] (if n (inc n) 1)) это всего лишь (fnil inc 0). - person amalloy; 14.04.2013
comment
@David: нет, он возвращает новую игру с инициализированной структурой данных :lib. Обратите внимание, что этот код находится в макроблоке as->, поэтому game повторно привязывается к результату каждой операции. Это выглядит немного похоже на изменяющееся состояние, но на самом деле это чистая функция. - person mikera; 14.04.2013