Какие переменные влияют на функцию 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, конечно, все ставки выключены.

Я могу думать о трех категориях решений:

  1. Получить информацию от компилятора

    Я предполагаю, что компилятор выполняет сканирование функций для определения необходимой информации, потому что, если я пытаюсь сослаться на несуществующий 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, и я хотел бы получить доступ к этой информации.

  2. Разобрать исходный код, пройтись по синтаксическому дереву и записать, когда ссылка на Var

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

  3. Изучите механизм 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), который не зависит от какого-либо глобального состояния.

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