Какие переменные влияют на функцию Clojure? - PullRequest
3 голосов
/ 28 февраля 2012

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

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

  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 ]

5 голосов
/ 28 февраля 2012

Что касается вашего первого пункта, вы можете рассмотреть возможность использования библиотеки 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*}
0 голосов
/ 28 февраля 2012

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

Например:

(def ^:dynamic *increment* 3)
(defn f
  ([x]
     (f x *increment*))
  ([x y]
     (+ x y)))

Таким образом, вы можете написать все свои тесты для (f x y), который не зависит от какого-либо глобального состояния.

...