Как программно определить, какие переменные могут повлиять на результаты функции, определенной в Clojure?
Рассмотрим это определение функции Clojure:
(def ^:dynamic *increment* 3)
(defn f [x]
(+ x *increment*))
Это функция x
, но также *increment*
(а также clojure.core/+
(1) ; но меня это меньше беспокоит). При написании тестов для этой функции я хочу убедиться, что я контролирую все соответствующие входы, поэтому я делаю что-то вроде этого:
(assert (= (binding [*increment* 3] (f 1)) 4))
(assert (= (binding [*increment* -1] (f 1)) 0))
(Представьте, что *increment*
- это значение конфигурации, которое кто-то может разумно изменить; я не хочу, чтобы тесты этой функции нуждались в изменении, когда это происходит.)
У меня вопрос: как мне написать утверждение, что значение (f 1)
может зависеть от *increment*
, но не от любого другого Var? Потому что я ожидаю, что однажды кто-то проведет рефакторинг некоторого кода и вызовет функцию
(defn f [x]
(+ x *increment* *additional-increment*))
и пренебрегайте обновлением теста, и я хотел бы, чтобы тест не прошел, даже если *additional-increment*
равен нулю.
Это, конечно, упрощенный пример & ndash; в большой системе может быть много динамических Vars, и на них можно ссылаться через длинную цепочку вызовов функций. Решение должно работать, даже если f
вызывает g
, который вызывает h
, который ссылается на Var. Было бы здорово, если бы он не утверждал, что (with-out-str (prn "foo"))
зависит от *out*
, но это менее важно. Если анализируемый код вызывает eval
или использует взаимодействие Java, конечно, все ставки выключены.
Я могу думать о трех категориях решений:
Получить информацию от компилятора
Я полагаю, что компилятор сканирует определения функций для получения необходимой информации, потому что, если я пытаюсь сослаться на несуществующий Var, он выдает:
user=> (defn g [x] (if true x (+ *foobar* x)))
CompilerException java.lang.RuntimeException: Unable to resolve symbol: *foobar* in this context, compiling:(NO_SOURCE_PATH:24)
Обратите внимание, что это происходит во время компиляции и независимо от того, будет ли когда-либо выполняться код, вызывающий нарушение. Таким образом, компилятор должен знать, на что Vars потенциально ссылается функция, и я хотел бы иметь доступ к этой информации.
Разобрать исходный код, пройтись по синтаксическому дереву и записать, когда есть ссылка на Var
Потому что код - это данные и все такое. Я предполагаю, что это означает вызов macroexpand
и обработку каждого примитива Clojure и любого вида синтаксиса, который они принимают. Это так похоже на фазу компиляции, что было бы здорово иметь возможность вызывать части компилятора или каким-то образом добавлять мои собственные хуки в компилятор.
Изучите механизм Var, выполните тест и посмотрите, к какому из Vars обращаются
Не так полно, как другие методы (что, если Var используется в ветке кода, которую мой тест не может выполнить?), Но этого будет достаточно. Я полагаю, мне нужно было бы переопределить def
, чтобы получить что-то, что действует как Var, но как-то записывает его обращения.
(1) На самом деле эта конкретная функция не изменится, если вы выполните повторное связывание +
; но в Clojure 1.2 вы можете обойти эту оптимизацию, сделав ее (defn f [x] (+ x 0 *increment*))
, и тогда вы сможете повеселиться с (binding [+ -] (f 3))
. В Clojure 1.3 при попытке повторного связывания +
выдается ошибка.