Лучшая практика для глобальных объектов в 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 предназначены не только для многопоточного кода, но в основном для любого глобального, требующего неизменности. Есть ли для этого лучший способ? Я мог бы попытаться реорганизовать код, чтобы просто использовать привязку или разрешить, но это может быть особенно сложно для некоторых приложений.


person Community    schedule 16.07.2010    source источник


Ответы (2)


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

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

person Community    schedule 16.07.2010
comment
Я не новичок в описанном вами функциональном подходе. Но иногда полезно удобство глобального расположения этого удерживаемого состояния. Все функциональные подходы ломаются по краям, наиболее часто встречающимся случаем является ввод-вывод. Вы можете считать это частным случаем ввода-вывода, поскольку он фактически глобален для всех потоков. Не поймите меня неправильно, я предпочитаю функциональный подход, и мой пример использования приведенной выше ссылки является чрезмерно упрощенным, поэтому я по большей части согласен с вами. - person Jeremy Wall; 17.07.2010
comment
передача значения всем функциям, безусловно, хорошая альтернатива, но иногда мне кажется, что глобальная переменная просто более практична, чем многократная передача большого количества значений множеству функций. Это вопрос вкуса и толерантности к побочным эффектам. - person ChrisBlom; 19.02.2014

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

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

(def state (atom {}))

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

(defn update-state [key val]
  (swap! state assoc key val))
person Community    schedule 16.07.2010
comment
Это близко к моему предпочтительному подходу, я делаю то же самое, что и для объявления общего местоположения, чтобы избежать его перезаписи, и использую звездочки в имени, чтобы было ясно, что это общее местоположение. - person ChrisBlom; 19.02.2014
comment
Использование звездочек в имени символа теперь приводит к предупреждению компилятора, поскольку они зарезервированы для динамических переменных. - person spieden; 14.12.2016