Удалите первый элемент из атома вектора Clojure и верните его

У меня есть атом, обертывающий вектор элементов:

(def items (atom [1 2 3 4]))

Я хочу атомизировать первый элемент и вернуть его. Этот код иллюстрирует логику:

(let [x (first @items)]
  (swap! items #(subvec % 1))
  x)

Но приведенный выше код неверен, когда многие потоки соперничают друг с другом. Существует условие гонки между чтением и обновлением.

Как было сказано в этом ответе, атомы для нескоординированного синхронного доступа. Я надеялся, что это можно сделать с атомом вместо ref, потому что атом проще.

Есть ли решение, которое использует только атомы, а не refs? (Я собираюсь попробовать использовать часы и посмотреть, как это происходит.) Если ваш ответ настаивает на необходимости ссылки, не могли бы вы объяснить, почему требуется ref, хотя ссылки предлагаются, когда требуется "Скоординированный синхронный доступ ко многим идентификаторам" "(такая же ссылка, как указано выше).

Это отличается от других связанных вопросов, таких как Как обновить векторный элемент атома в Clojure? и Лучший способ удалить элемент в списке для атома в Clojure, потому что я хочу обновить векторный атом и вернуть значение.

Ответ 1

Цикл спина с compareAndSet используется для swap! атома. Clojure также обеспечивает более низкий уровень compare-and-set! для атомов, которые вы можете использовать, чтобы выполнить свой собственный цикл вращения и вернуть как старое, так и новое значение.

(defn swap*!
  "Like swap! but returns a vector of [old-value new-value]"
  [atom f & args]
  (loop [] 
    (let [ov @atom 
          nv (apply f ov args)]
      (if (compare-and-set! atom ov nv)
        [ov nv]
        (recur)))))

(defn remove-first-and-return [atom]
  (let [[ov nv] (swap*! atom subvec 1)]
    (first ov)))

Ответ 2

Если вам нужно использовать атом, используйте локально инкапсулированный атом для хранения первого элемента значения in-transaction выигрышной транзакции.

(let [f-atom (atom nil)]
  (swap! items-atom #(do (reset! f-atom (first %)) 
                         (rest %)))
  @f-atom)

В качестве альтернативы, выполните то же самое с транзакционным блоком ref и a dosync:

(dosync
  (let [f (first @items-ref)]
    (alter items-ref rest)
    f)))

Здесь, если транзакция завершилась неудачно, так как операция параллельной записи завершилась успешно, транзакция не возвращает или не влияет на ref до тех пор, пока ее нельзя было бы повторить, чтобы операции чтения и записи выполнялись без прерывания другой операцией записи.

Ответ 3

Мое решение для этого - хранить любое возвращаемое значение в метаданных содержащегося значения. Я не знаю, как это идиоматично, и он, очевидно, работает только для классов, реализующих интерфейс IMeta.

(defn pop-head!
  [a]
  (-> (swap! a #(with-meta (subvec % 1) {:head (% 0)}))
      (meta)
      :head))

Это зависит от того, что swap! возвращает теперь сохраненное значение атома. Попробуем:

(def a (atom [1 2 3 4]))
(pop-head! a) ;; => 1
(pop-head! a) ;; => 2
(pop-head! a) ;; => 3
(pop-head! a) ;; => 4
(pop-head! a) ;; => IndexOutOfBoundsException...

Да, вы можете справиться с этим делом.;)

Ответ 4

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

Вы можете создать часы на атоме с add-watch, который отправит событие как с старыми, так и с новыми значениями.