Наилучшая практика для глобалов в clojure (refs против alter-var-root)? - PullRequest
26 голосов
/ 16 июля 2010

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

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

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

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

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

Ответы [ 2 ]

28 голосов
/ 16 июля 2010

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

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

(def state (atom {}))

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

(defn update-state [key val]
  (swap! state assoc key val))
22 голосов
/ 16 июля 2010

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

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

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

Сложно думать о коде таким образом, если вы привыкли к стилю программирования «мутировать кучу объектов / памяти», но как только вы освоите его, он становитсяОтносительно просто организовать ваши программы таким образом.Ваш код обычно оказывается таким же простым или более простым, чем версия того же кода с глобальной мутацией.

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

(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: просто введите нужную «макетную» адресную книгу и посмотрите, что она печатает.Вам не нужно заботиться о каком-либо глобальном состоянии или о чем-либо, кроме функции, которую вы тестируете в данный момент.

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

...