Должен ли я использовать функцию или макрос для проверки аргументов в Clojure?

У меня есть группа числовых функций в Clojure, что я хочу проверить аргументы для. Существует множество типов аргументов, ожидаемых функциями, таких как положительные целые числа, проценты, последовательности чисел, последовательности ненулевых чисел и т.д. Я могу проверить аргументы любой отдельной функции:

  • Написание кода проверки в функция.
  • Написание общего функции, передавая его аргументы и ожидаемые типы.
  • Написание макроса общего назначения, передавая ему аргументы и ожидаемые типы.
  • Другие, о которых я не думал.

Некоторые Lisp код Ларри Хантера - хороший пример №3. (Ищите макрос test-variables.)

Моя интуиция заключается в том, что макрос более уместен из-за контроля над оценкой и возможности делать вычисления времени компиляции, а не делать все это во время выполнения. Но я не столкнулся с прецедентом для кода, который я пишу, который, кажется, требует его. Мне интересно, стоит ли писать такой макрос.

Любые предложения?

Ответ 1

Clojure уже имеет (недокументированную, возможно, подлежащую изменению) поддержку пре- и пост-условий на fn s.

user> (defn divide [x y]
        {:pre [(not= y 0)]}
        (/ x y))
user> (divide 1 0)
Assert failed: (not= y 0)
   [Thrown class java.lang.Exception]

Какой-то уродливый.

Я бы, вероятно, написал макрос, чтобы я мог сообщить, какие тесты потерпели неудачу кратким способом (цитата и печать теста в буквальном смысле). Код CL, с которым вы связались, выглядит довольно неприятно с этим огромным выражением. По-моему, здесь лучше бы использовать несколько методов. Вы можете легко собрать что-то подобное вместе.

(defmacro assert* [val test]
  `(let [result# ~test]              ;; SO`s syntax-highlighting is terrible
     (when (not result#)
       (throw (Exception.
               (str "Test failed: " (quote ~test)
                    " for " (quote ~val) " = " ~val))))))

(defmulti validate* (fn [val test] test))

(defmethod validate* :non-zero [x _]
  (assert* x (not= x 0)))

(defmethod validate* :even [x _]
  (assert* x (even? x)))

(defn validate [& tests]
  (doseq [test tests] (apply validate* test)))

(defn divide [x y]
  (validate [y :non-zero] [x :even])
  (/ x y))

Тогда:

user> (divide 1 0)
; Evaluation aborted.
; Test failed: (not= x 0) for x = 0
;   [Thrown class java.lang.Exception]

user> (divide 5 1)
; Evaluation aborted.
; Test failed: (even? x) for x = 5
;   [Thrown class java.lang.Exception]

user> (divide 6 2)
3

Ответ 2

Несколько мыслей.

У меня есть ощущение, что это зависит от сложности и количества валидаций, а также от характера функций.

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

Например, вы пишете:

  • валидатор, чтобы убедиться, что список не пуст,
  • валидатор, чтобы убедиться, что значение больше нуля,
  • используйте 1 и 2, чтобы убедиться, что значение является непустым списком значений больше нуля.

Если вы просто делаете огромное количество простых проверок, а ваша проблема - многословие (например, у вас есть 50 функций, для которых все требуются ненулевые целые числа), то макрос, вероятно, имеет больше смысла.

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

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

Ответ 3

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

(with-function-validators [test1 test2 test4]  
    (defn fun1 [arg1 arg2] 
        (do-stuff))
    (defn fun2 [arg1 arg2] 
        (do-stuff))
    (defn fun3 [arg1 arg2] 
        (do-stuff)))