Когда в Clojure следует использовать идиому временного перепривязки-специального-var?

Я заметил, что некоторые библиотеки, такие как clojure-twitter, используют специальные переменные (те, которые предназначены для динамического связывания, которые окружены звездочками) для аутентификации oauth. Вы сохраняете свою аутентификацию в var и затем используете (with-oauth myauth ..). Я думаю, что это очень хорошее решение для такого рода проблем, потому что вы можете перепривязать auth var для каждого пользователя приложения.

Я выбрал аналогичный маршрут в почтовом клиенте, который писал. У меня есть специальная переменная с именем сеанс, которую я привязываю к карте с сеансом текущего пользователя и информацией о пользователе, и есть различные важные функции, которые используют информацию из этого var. Я написал макрос with-session, чтобы временно привязать его в контексте набора форм, переданных with-session. Оказывается, это довольно чистое решение (для меня).

Итак, мой вопрос заключается в следующем: я делаю это обряд? Это плохое дизайнерское решение, или это одно из предполагаемых применений специальных переменных?

3 ответа

Решение

Вы, кажется, делаете это совершенно правильно. На самом деле, есть несколько встроенных макросов / contrib, которые работают аналогично, скажем, with-out-str или же clojure.contrib.sql/with-connection, Последний является довольно важной частью современной инфраструктуры Clojure, поэтому многие идиомы изучают его.

Важно помнить, что потоки, которые вы запускаете, находясь в bindings / with-bindings форма не наследует значения отскока для рассматриваемых переменных; скорее они видят корневые привязки. Если вы хотите распространять свои привязки на рабочие потоки / агенты, либо передавайте их явно (скажем, в качестве аргументов функции), либо используйте bound-fn,

Каждый раз, когда вы создаете глобальную переменную, которую планируете повторно связать, вы добавляете дополнительный неявный аргумент к каждой функции, которая обращается к этой переменной. В отличие от правильных (явных) аргументов, этот скрытый аргумент не отображается в сигнатуре функций, и может быть мало признаков того, что функция его использует. Ваш код становится менее "функциональным"; вызов одной и той же функции с одинаковыми аргументами может привести к разным возвращаемым значениям в зависимости от текущего состояния этих глобальных динамических переменных.

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

Недостатком является то, что ваш код труднее читать, тестировать, использовать и отлаживать. И ваш код становится потенциально более подверженным ошибкам; легко забыть связать или повторно связать переменную перед вызовом функции, которая ее использует, но не так легко забыть передать session параметр, когда он находится в арглисте.

Таким образом, вы получите загадочные ошибки и странные неявные зависимости между функциями. Рассмотрим этот сценарий:

user> (defn foo [] (when-not (:logged-in *session*) (throw (Exception. "Access denied!"))))
#'user/foo
user> (defn bar [] (foo))
#'user/bar
user> (defn quux [] (bar))
#'user/quux
user> (quux)
; Evaluation aborted.  ;; Access denied!

Поведение quux неявно зависит от того, имеет ли сеанс значение, но вы не узнаете этого, если не будете копаться в каждой функции quux вызовы, и каждая функция, которую вызывают эти функции. Представьте себе цепочку вызовов глубиной 10 или 20, с одной функцией внизу в зависимости от *session*, Получайте удовольствие отлаживая это.

Если вместо этого у вас было (defn foo [session] ...), (defn bar [session] ...), (defn quux [session] ...), было бы сразу очевидно, что если вы позвоните quuxВам лучше подготовить сессию.

Лично я бы использовал явные аргументы, если бы у меня не было сильного, нормального значения по умолчанию, которое использовалось бы тоннами функций, которое я планировал очень редко или никогда не связывал. (Например, было бы глупо передавать STDOUT в качестве явного аргумента каждой функции, которая хочет что-либо напечатать.)

Функции привязки отлично подходят для тестового кода.

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

(defmacro with-fake-prng [ & exprs ]
  "replaces the prng with one that produces consisten results"
  `(binding [com.cryptovide.split/get-prng (fn [] (cycle [1 2 3]))
             com.cryptovide.modmath/mody 719
             com.cryptovide.modmath/field-size 10]
        ~@exprs))

(is (= (with-fake-prng (encrypt-string "asdf")) [23 54 13 63]))  

При использовании привязок полезно помнить, что они привязываются только к текущему потоку, поэтому при запуске чего-либо в pmap, использующем пул потоков, вы можете потерять свои привязки. Если у вас есть код, который строит строку параллельно, как это:

(with-out-str
    (pmap process-data input))

Использование этого невинного зацикливания \ p перед картой приведет к тому, что привязка исчезнет, ​​потому что она запустит функцию process-data в нескольких потоках из пула потоков.

РЕДАКТИРОВАТЬ: Михал Marczyk указывает макрос bound-fn, который вы можете использовать, чтобы не потерять привязки при использовании потоков.

Другие вопросы по тегам