testing - ¿Cómo probar una macro clojure que usa gensyms?
macros (2)
No pruebe cómo funciona (su expansión), pruebe que funciona. Si prueba la expansión particular, está encadenado a esa estrategia de implementación; en su lugar, simplemente prueba que (m1 2 inc)
devuelve 3, y cualquier otro caso de prueba que sea necesario para consolar tu conciencia, y luego puedes estar contento de que tu macro funcione.
Quiero probar una macro que usa gensyms. Por ejemplo, si quiero probar esto:
(defmacro m1
[x f]
`(let [x# ~x]
(~f x#)))
Puedo usar macro-expansión ...
(macroexpand-1 ''(m1 2 inc))
...Llegar...
(clojure.core/let [x__3289__auto__ 2] (inc x__3289__auto__))
Eso es fácil de verificar para una persona como correcta.
Pero, ¿cómo puedo probar esto de una manera práctica, limpia y automatizada ? El gensym no es estable.
(Sí, sé que el ejemplo macro particular no es convincente, pero la pregunta sigue siendo justa).
Me doy cuenta de que las expresiones de Clojure se pueden tratar como datos (es un lenguaje homoicónico), así que puedo separar el resultado de esta manera:
(let [result (macroexpand-1 ''(m1 2 inc))]
(nth result 0) ; clojure.core/let
(nth result 1) ; [x__3289__auto__ 2]
((nth result 1) 0) ; x__3289__auto__
((nth result 1) 0) ; 2
(nth result 2) ; (inc x__3289__auto__)
(nth (nth result 2) 0) ; inc
(nth (nth result 2) 1) ; x__3289__auto__
)
Pero esto es difícil de manejar. ¿Hay mejores formas? ¿Tal vez hay bibliotecas de ''validación'' de estructuras de datos que podrían ser útiles? Tal vez la desestructuración haría esto más fácil? ¿Programación lógica?
ACTUALIZACIÓN / COMENTARIO :
Aunque aprecio el consejo de personas con experiencia que dicen "no prueben la macro expansión en sí", no responde mi pregunta directamente.
¿Qué tiene de malo la "prueba unitaria" de una macro al probar la macro-expansión? Probar la expansión es razonable, y de hecho, muchas personas prueban sus macros de esa manera "a mano" en el REPL, así que ¿por qué no probarlo también automáticamente? No veo una buena razón para no hacerlo. Admito que probar la macroexpansión es más que frágil que probar el resultado, pero hacer lo primero aún puede tener valor. También puede probar la funcionalidad, ¡puede hacer ambas cosas! Esta no es una decisión cualquiera.
Aquí está mi explicación psicológica. Una de las razones por las que las personas no prueban la macroexpansión es que actualmente es un poco doloroso. En general, las personas a menudo racionalizan en contra de hacer algo cuando parece difícil, independientemente de su valor intrínseco. Sí, ¡es exactamente por eso que hice esta pregunta! Si fuera fácil, creo que la gente lo haría más a menudo. Si fuera fácil, sería menos probable que racionalicen dando respuestas diciendo que "no vale la pena hacerlo".
También entiendo el argumento de que "no deberías escribir una macro compleja". Por supuesto. Pero esperemos que las personas no lleguen a pensar que "si fomentamos una cultura de no probar macros, eso evitará que las personas escriban complejas". Tal argumento sería tonto. Si tiene una macro expansión compleja, probar que funciona como espera es algo sensato. Personalmente, no estoy debajo de probar incluso las cosas simples, porque a menudo me sorprende que los errores puedan provenir de errores simples.
Esto se puede hacer con metadatos. Su macro genera una lista, que puede tener metadatos adjuntos. Simplemente agregue las asignaciones gensym-> var a eso y luego úselos para probar.
Entonces tu macro se vería así:
(defmacro m1 [x f]
(let [xsym (gensym)]
(with-meta
`(let [~xsym ~x]
(~f ~xsym))
{:xsym xsym})))
La salida de la macro ahora tiene un mapa contra ella con los gensyms:
(meta (macroexpand-1 ''(m1 a b)))
=> {:xsym G__24913}
Para probar la expansión de macros, harías algo como esto:
(let [out (macroexpand-1 `(m1 a b))
xsym (:xsym (meta out))
target `(clojure.core/let [~xsym a] (b ~xsym))]
(= out target))
Para abordar la pregunta por qué querría hacer esto: La forma en que normalmente escribo una macro es generar primero el código objetivo (es decir, lo que deseo que genere la macro), probar que haga lo correcto y luego generar la macro a partir de ese . Tener el código conocido por adelantado me permite hacer TDD contra la macro; en particular, puedo modificar la macro, ejecutar las pruebas y, si fallan, clojure.test
me mostrará el objetivo real y el que puedo inspeccionar visualmente.