Какие переменные влияют на функцию Clojure?
Как программно определить, какие переменные могут повлиять на результаты функции, определенной в 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*
но не на любом другом вар? Потому что я ожидаю, что однажды кто-то проведет рефакторинг некоторого кода и заставит функцию
(defn f [x]
(+ x *increment* *additional-increment*))
и пренебречь обновлением теста, и я хотел бы, чтобы тест не прошел, даже если *additional-increment*
это ноль.
Это, конечно, упрощенный пример - в большой системе может быть много динамических Vars, и на них можно ссылаться через длинную цепочку вызовов функций. Решение должно работать, даже если f
звонки g
какие звонки h
который ссылается на вар. Было бы здорово, если бы он не утверждал, что (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 предпринимается попытка привязки +
выдает ошибку.
2 ответа
Что касается вашего первого пункта, вы можете рассмотреть возможность использования analyze
библиотека. С его помощью вы можете легко определить, какие динамические переменные используются в выражении:
user> (def ^:dynamic *increment* 3)
user> (def src '(defn f [x]
(+ x *increment*)))
user> (def env {:ns {:name 'user} :context :eval})
user> (->> (analyze-one env src)
expr-seq
(filter (op= :var))
(map :var)
(filter (comp :dynamic meta))
set)
#{#'user/*increment*}
Я знаю, что это не ответит на ваш вопрос, но не будет ли намного проще просто предоставить две версии функции, в которой одна версия не имеет свободных переменных, а другая версия вызывает первую с соответствующей версией уровень определяет?
Например:
(def ^:dynamic *increment* 3)
(defn f
([x]
(f x *increment*))
([x y]
(+ x y)))
Таким образом, вы можете написать все свои тесты против (f x y)
, который не зависит от какого-либо глобального состояния.