Подтвердить что ты не робот

Лучшая практика для globals в clojure, (refs vs alter-var-root)?

В последнее время я обнаружил, что использую следующую идиому в коде clojure.

(def *some-global-var* (ref {}))

(defn get-global-var []
  @*global-var*)

(defn update-global-var [val]
  (dosync (ref-set *global-var* val)))

В большинстве случаев это даже не многопоточный код, которому может потребоваться транзакционная семантика, которая дает вам. Он просто чувствует, что refs - это больше, чем сквозной код, но в основном для любого глобального, который требует непреложности. Есть ли более эффективная практика для этого? Я мог бы попытаться реорганизовать код, чтобы просто использовать привязку или позволить, но это может стать особенно сложным для некоторых приложений.

4b9b3361

Ответ 1

У ваших функций есть побочные эффекты. Вызов их дважды с одинаковыми входами может давать разные значения возврата в зависимости от текущего значения *some-global-var*. Это затрудняет проверку и рассуждение, особенно после того, как вы плаваете вокруг одного из этих глобальных варов.

Люди, вызывающие ваши функции, могут даже не знать, что ваши функции зависят от значения глобального var, не проверяя источник. Что делать, если они забывают инициализировать глобальный var? Легко забыть. Что делать, если у вас есть два набора кода, которые пытаются использовать библиотеку, которая опирается на эти глобальные вары? Вероятно, они будут переходить друг на друга, если вы не используете binding. Вы также добавляете накладные расходы каждый раз, когда получаете доступ к данным из ссылки.

Если вы напишете свой побочный эффект кода, эти проблемы исчезнут. Функция стоит сама по себе. Это легко проверить: передайте ему несколько входов, проверьте выходы, они всегда будут одинаковыми. Легко понять, от каких функций зависит функция: все они находятся в списке аргументов. И теперь ваш код является потокобезопасным. И, вероятно, работает быстрее.

Трудно думать об этом коде, если вы привыкли к стилю программирования "mutate the bunch of objects/memory", но как только вы его повесили, становится довольно просто организовать ваши программы таким образом, Ваш код обычно заканчивается так же просто или проще, чем версия с глобальным мутацией того же кода.

Вот очень надуманный пример:

(def *address-book* (ref {}))

(defn add [name addr]
  (dosync (alter *address-book* assoc name addr)))

(defn report []
  (doseq [[name addr] @*address-book*]
    (println name ":" addr)))

(defn do-some-stuff []
  (add "Brian" "123 Bovine University Blvd.")
  (add "Roger" "456 Main St.")
  (report))

Глядя на do-some-stuff изолированно, что он делает? Есть много вещей, которые происходят неявно. На этом пути лежат спагетти. Возможная версия:

(defn make-address-book [] {})

(defn add [addr-book name addr]
  (assoc addr-book name addr))

(defn report [addr-book]
  (doseq [[name addr] addr-book]
    (println name ":" addr)))

(defn do-some-stuff []
  (let [addr-book (make-address-book)]
    (-> addr-book
        (add "Brian" "123 Bovine University Blvd.")
        (add "Roger" "456 Main St.")
        (report))))

Теперь ясно, что делает do-some-stuff, даже в изоляции. Вы можете иметь столько адресных книг, которые будут плавать вокруг, как вы хотите. Несколько потоков могут иметь свои собственные. Вы можете использовать этот код из нескольких пространств имен безопасно. Вы не можете забыть инициализировать адресную книгу, потому что вы передаете ее в качестве аргумента. Вы можете легко протестировать report: просто передайте нужную "макет" адресную книгу и посмотрите, что она печатает. Вам не нужно заботиться о каком-либо глобальном состоянии или о чем-либо, кроме функции, которую вы тестируете на данный момент.

Если вам не нужно координировать обновления структуры данных из нескольких потоков, обычно нет необходимости использовать refs или глобальные vars.

Ответ 2

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

например. для измененной карты пар ключ/значение я бы использовал:

(def state (atom {}))

(defn get-state [key]
  (@state key))

(defn update-state [key val]
  (swap! state assoc key val))