2009-10-28 12 views
11

Tengo un grupo de funciones numéricas en Clojure para las que quiero validar los argumentos. Existen numerosos tipos de argumentos esperados por las funciones, como números enteros positivos, porcentajes, secuencias de números, secuencias de números distintos de cero, etc. Puedo validar los argumentos a cualquier función individual por:¿Debo usar una función o una macro para validar argumentos en Clojure?

  1. Escribiendo código de validación directamente en la función.
  2. Escribiendo una función de propósito general , pasándole los argumentos y tipos esperados.
  3. Escribiendo una macro de propósito general, pasando los argumentos y tipos esperados.
  4. Otros en los que no he pensado.

Algunos Lisp code de Larry Hunter es un buen ejemplo de # 3. (Busque la macro test-variables)

Mi intuición es que una macro es más apropiada debido al control sobre la evaluación y la posibilidad de realizar cálculos en tiempo de compilación en lugar de hacerlo todo en tiempo de ejecución. Pero no he encontrado un caso de uso para el código que estoy escribiendo que parece requerirlo. Me pregunto si vale la pena el esfuerzo escribir tal macro.

¿Alguna sugerencia?

Respuesta

13

Clojure ya tiene compatibilidad (no documentada, tal vez sujeta a cambios) para condiciones previas y posteriores en 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] 

Tipo de ugly though.

Probablemente escribiría una macro solo para poder informar qué pruebas fallaron de manera concisa (citar e imprimir la prueba literalmente). El código CL al que vinculó parece bastante desagradable con esa enorme declaración de caso. Multimétodos sería mejor aquí en mi opinión. Puedes lanzar algo como esto juntos muy fácilmente.

(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)) 

continuación:

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 
+0

Agradable; No sabía que – pmf

+0

Gracias por otra gran respuesta. Esto hace exactamente lo que yo quería hacer. También ilustra uno de los motivos por los que una macro es una mejor solución. Pensando en cómo desambiguar el argumento del problema si una función requiere dos o más con las mismas características, digamos dos enteros positivos, la ruta de la macro le permite poner el nombre del argumento del problema en el mensaje de error, no solo el valor. No creo que puedas hacer eso con una función. – clartaq

+1

Las condiciones previas y posteriores están documentadas aquí http://clojure.org/special_forms Busque el mapa de condiciones. – Chouser

3

Solo algunas consideraciones.

Tengo la sensación de que depende de la complejidad y el número de validaciones, y la naturaleza de las funciones.

Si está realizando validaciones muy complejas, debe separar los validadores de sus funciones. El razonamiento es que puedes usar los más simples para construir otros más complejos.

Por ejemplo, se escribe:

  1. un validador para asegurarse de que la lista no está vacía,
  2. un validador para asegurarse de que un valor es mayor que cero,
  3. uso 1 y 2 para asegúrese de que un valor sea una lista no vacía de valores mayor que cero.

Si solo hace una gran cantidad de validaciones simples, y su problema es verbosidad (por ejemplo, tiene 50 funciones que requieren enteros distintos de cero), entonces una macro probablemente tenga más sentido.

Otra cosa a tener en cuenta es que la evaluación de la función es que Clojure está ansioso. Puede obtener un aumento de rendimiento en algunos casos al no evaluar algunos parámetros si sabe que la función fallará, o si algunos parámetros no son necesarios en función de los valores de otros parámetros. P.ej. el todo? predicado no necesita evaluar cada valor en una colección.

Finalmente, para abordar "otros en los que no has pensado". Clojure admite un envío genérico pbased en una función de despacho. Esa función podría enviar al código apropiado, o mensajes de error basados ​​en cualquier cantidad de factores.

1

Un caso en el que necesita una macro habría si desea modificar el lenguaje para agregar automáticamente las pruebas para cualquier función definida dentro de un bloque, como este:

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