Лучшая практика для globals в clojure, (судьи по сравнению с 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)))

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

28
задан Jeremy Wall 16 July 2010 в 18:43
поделиться

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

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

24
ответ дан 28 November 2019 в 03:08
поделиться

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

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

(def state (atom {}))

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

(defn update-state [key val]
  (swap! state assoc key val))
30
ответ дан 28 November 2019 в 03:08
поделиться
Другие вопросы по тегам:

Похожие вопросы: