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

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

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

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

Ответы [ 3 ]

7 голосов
/ 15 февраля 2010

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

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

3 голосов
/ 15 февраля 2010

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

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

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

1 голос
/ 16 февраля 2010

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

В своем тестовом коде я широко использую функции обвязки обертки, чтобы делать такие вещи, как макет генератора случайных чисел, использовать фиксированный размер блока и т. Д., Чтобы я мог на самом деле протестировать функцию шифрования на основе известного вывода. </p> <pre><code>(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 , который можно использовать, чтобы не потерять привязки при использовании потоков.

...