Когда в 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, который вы можете использовать, чтобы не потерять привязки при использовании потоков.