2011-01-13 26 views
6

Deseo enviar var-args de una función a una macro, aún como var-args. Aquí está mi código:Cómo expandir una secuencia (var-args) en elementos distintos

(defmacro test-macro 
[& args] 
`(println (str "count=" ~(count args) "; args=" [email protected]))) 

(defn test-fn-calling-macro 
[& args] 
(test-macro args)) 

La salida de (test-macro "a" "b" "c") es lo que quiero: count=3; args=abc

La salida de (test-fn-calling-macro "a" "b" "c") es: count=1; args=("a" "b" "c"), ya que args se envía como un solo argumento a la macro. ¿Cómo puedo expandir estos argumentos en mi función para llamar a la macro con los 3 argumentos?

Supongo que me está perdiendo una simple función central, pero no puedo encontrarla. Gracias


EDIT 2 - Mi código "real", que se muestra en la sección Editar Esto no es una situación válida para utilizar esta técnica.

Como ha señalado @ Brian, la macro xml-to-cass puede ser sustituida por una función como esta:

(defn xml-to-cass 
    [zipper table key attr & path] 
    (doseq [v (apply zf/xml-> zipper path)] (cass/set-attr! table key attr v))) 

EDITAR - la siguiente sección va más allá de mi pregunta original, pero alguna idea es bienvenida

El código anterior es simplemente el más simple que podría venir para identificar mi problema. Mi código real trata de clj-cassandra y zip-filter. También puede parecer sobre ingeniería, pero es solo un proyecto de juguete y estoy tratando de aprender el idioma al mismo tiempo.

Quiero analizar un XML encontrado en mlb.com e insertar valores encontrados en una base de datos de cassandra. Aquí está mi código y el pensamiento detrás de él.

Paso 1 - Función que funciona bien, pero contiene la duplicación de código

(ns stats.importer 
    (:require 
    [clojure.xml :as xml] 
    [clojure.zip :as zip] 
    [clojure.contrib.zip-filter.xml :as zf] 
    [cassandra.client :as cass])) 

(def root-url "http://gd2.mlb.com/components/game/mlb/year_2010/month_05/day_01/") 

(def games-table (cass/mk-cf-spec "localhost" 9160 "mlb-stats" "games")) 

(defn import-game-xml-1 
    "Import the content of xml into cassandra" 
    [game-dir] 
    (let [url (str root-url game-dir "game.xml") 
     zipper (zip/xml-zip (xml/parse url)) 
     game-id (.substring game-dir 4 (- (.length game-dir) 1))] 
    (doseq [v (zf/xml-> zipper (zf/attr :type))] (cass/set-attr! games-table game-id :type v)) 
    (doseq [v (zf/xml-> zipper (zf/attr :local_game_time))] (cass/set-attr! games-table game-id :local_game_time v)) 
    (doseq [v (zf/xml-> zipper :team [(zf/attr= :type "home")] (zf/attr :name_full))] (cass/set-attr! games-table game-id :home_team v)))) 

El parámetro a import-game-xml-1 puede ser por ejemplo "gid_2010_05_01_colmlb_sfnmlb_1/". Elimino el "gid_" y la barra al final para convertirlo en la clave de los juegos ColumnFamily en mi base de datos.

Encontré que el 3 doseq era mucha duplicación (y debería haber más de 3 en la versión final). Así que el código de plantillas usando una macro parecía apropiado aquí (corríjanme si me equivoco).

Paso 2 - Presentación de una macro para crear plantillas de código (todavía funciona)

(defmacro xml-to-cass 
    [zipper table key attr & path] 
    `(doseq [v# (zf/xml-> ~zipper [email protected])] (cass/set-attr! ~table ~key ~attr v#))) 

(defn import-game-xml-2 
    "Import the content of xml into cassandra" 
    [game-dir] 
    (let [url (str root-url game-dir "game.xml") 
     zipper (zip/xml-zip (xml/parse url)) 
     game-id (.substring game-dir 4 (- (.length game-dir) 1))] 
    (xml-to-cass zipper games-table game-id :type (zf/attr :type)) 
    (xml-to-cass zipper games-table game-id :local_game_time (zf/attr :local_game_time)) 
    (xml-to-cass zipper games-table game-id :home_team :team [(zf/attr= :type "home")] (zf/attr :name_full)))) 

Creo que es una mejora, pero todavía veo cierta duplicación de siempre reutilizando los mismos 3 parámetros en mis llamadas a xml-to-cass. Es ahí donde introduje una función intermedia para cuidar de ellos.

Paso 3 - Adición de una función para llamar a la macro (el problema es aquí)

(defn import-game-xml-3 
    "Import the content of xml into cassandra" 
    [game-dir] 
    (let [url (str root-url game-dir "game.xml") 
     zipper (zip/xml-zip (xml/parse url)) 
     game-id (.substring game-dir 4 (- (.length game-dir) 1)) 
     save-game-attr (fn[key path] (xml-to-cass zipper games-table game-id key path))] 
    (save-game-attr :type (zf/attr :type)) ; works well because path has only one element 
    (save-game-attr :local_game_time (zf/attr :local_game_time)) 
    (save-game-attr :home :team [(zf/attr= :type "home"] (zf/attr :name_full))))) ; FIXME this final line doesn't work 

Respuesta

4

Aquí hay un código simple que puede ser esclarecedor.

Las macros son sobre generación de código. Si desea que eso suceda en tiempo de ejecución, por alguna razón, entonces debe compilar y evaluar el código en tiempo de ejecución. Esta puede ser una técnica poderosa.

(defmacro test-macro 
[& args] 
`(println (str "count=" ~(count args) "; args=" [email protected]))) 

(defn test-fn-calling-macro 
[& args] 
(test-macro args)) 

(defn test-fn-expanding-macro-at-runtime 
    [& args] 
    (eval (cons `test-macro args))) 

(defmacro test-macro-expanding-macro-at-compile-time 
    [& args] 
    (cons `test-macro args)) 

;; using the splicing notation 

(defmacro test-macro-expanding-macro-at-compile-time-2 
    [& args] 
    `(test-macro [email protected])) 

(defn test-fn-expanding-macro-at-runtime-2 
    [& args] 
    (eval `(test-macro [email protected]))) 



(test-macro "a" "b" "c") ;; count=3; args=abc nil 
(test-fn-calling-macro "a" "b" "c") ;; count=1; args=("a" "b" "c") nil 

(test-fn-expanding-macro-at-runtime "a" "b" "c") ; count=3; args=abc nil 
(test-macro-expanding-macro-at-compile-time "a" "b" "c") ; count=3; args=abc nil 
(test-macro-expanding-macro-at-compile-time-2 "a" "b" "c") ; count=3; args=abc nil 
(test-fn-expanding-macro-at-runtime "a" "b" "c") ; count=3; args=abc nil 

Si la contemplación de lo anterior no resulta esclarecedora, ¿podría sugerir algunos de mis artículos de blog?

En éste Voy a través de macros desde cero, y cómo el trabajo de clojure en particular:

http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-i-getting.html

Y en ésta se muestra por qué en tiempo de ejecución generación de código podría ser útil:

http://www.learningclojure.com/2010/09/clojure-faster-than-machine-code.html

+0

Muchas gracias por tomarse el tiempo para escribir eso. Me siento como iluminado ahora. También revisaré las publicaciones de tu blog. Lástima que no aprendí Lisp cuando era un niño en lugar de un adulto obstinado. Me cuesta mucho entender mi cerebro. Pero hasta ahora vale la pena el dolor. No me rendiré y espero comprender el pensamiento. – Damien

+0

¡Perdón por responder en kata! Hay sutilezas, y uno tiene que jugar por un tiempo antes de entender. No te preocupes por tu cerebro No toqué ninguna réplica hasta que cumplí 35 años. Me lleva alrededor de un año. Algún día intentarás escribir algo en un idioma con sintaxis y de pronto te darás cuenta de que te sientes atrapado por él. –

+1

La manera de asimilarlo realmente es pasar por el SICP y hacer todos los ejercicios. http://mitpress.mit.edu/sicp/ De hecho, apenas menciona las macros, pero sobre el momento en que escribe un intérprete de esquema en el esquema, comprenderá exactamente lo que está sucediendo en el evaluador, y en ese punto las macros son un corolario trivial. Incluso si su objetivo es programar en clojure u otro lisp complejo, esto es algo bueno que hacer, porque el esquema es tan simple que puede ver las ideas. Un poco como aprender Java primero si lo que realmente quieres hacer es C++. –

2

La forma típica de usar una colección como argumentos individuales a una función es utilizar (apply function my-list-o-args)

(defn test-not-a-macro [& args] 
    (print args)) 

(defn calls-the-not-a-macro [& args] 
    (apply test-not-a-macro args)) 

aunque no podrá usar apply porque test-macro es una macro. Para resolver este problema, deberá ajustar la macro de prueba en una llamada de función para que pueda aplicarla.

(defmacro test-macro [& args] 
    `(println [email protected])) 

(defn calls-test-macro [& args] 
    (eval (concat '(test-macro) (args)))) ;you almost never need eval. 

(defn calls-calls-test-macro [& args] 
    (calls-test-macro args)) 

Esto es realmente un buen ejemplo de una de las formas en que las macros son difíciles de componer. (algunos dirían que no se pueden componer limpiamente, aunque creo que es una exageración)

+0

pd: no estoy en mi REPL así que si esto está roto por favor edítelo (o coméntelo y lo arreglaré cuando llegue a casa) –

+0

Gracias. Intenté este código, pero recibo una 'ClassCastException: clojure.lang.ArraySeq no se puede convertir a clojure.lang.IFn'. ¿Alguna idea de por qué? Realmente pensé que habría una respuesta directa a mi pregunta, pero parece sorprendentemente compleja. Edité mi pregunta para proporcionar todos los detalles sobre mi código "real". Tal vez podría ayudar. Muchas gracias. – Damien

1

Sus necesidades no son claras. No veo por qué es necesaria una macro aquí para test-macro, a menos que intente imprimir formularios no evaluados suministrados a su macro.

Estas funciones proporcionan los resultados esperados, pero eso se debe a que sus datos de muestra fueron autoevaluados.

(defn test-args 
    [& args] 
    (println (format "count=%d; args=%s" 
        (count args) 
        (apply str args)))) 

o

(defn test-args 
    [& args] 
    (print (format "count=%d; args=" (count args))) 
    (doseq [a args] 
    (pr a)) 
    (newline)) 

Se pueden imaginar otras variantes para llegar al mismo resultado.

intentar llamar a esa función con algo que no evalúa a sí mismo, y tenga en cuenta el resultado:

(test-args (+ 1 2) (+ 3 4)) 

buscabais para ver los argumentos impresos como "37" o "(1 + 2) (+ 3 4) "?

Si, por el contrario, intentabas aprender sobre las macros y su expansión en general, en lugar de resolver este problema en particular, sintoniza tu pregunta para seguir investigando.

+0

Gracias por la respuesta. Mi código con la impresión fue completamente artificial y lo más simple posible para identificar el problema. No quería molestar a la gente con código irrelevante. Pero ya que me lo preguntaste, edité mi pregunta y proporcioné todos los detalles sobre por qué creía que necesitaba una macro. No puedo ser más específico; o) – Damien

2

Las macros no son mágicas. Son un mecanismo para convertir código en tiempo de compilación a código equivalente; no se usan en tiempo de ejecución. El dolor que sientes es porque estás tratando de hacer algo que no deberías tratar de hacer.

No conozco la biblioteca en cuestión, pero si cass/set-attr! es una función, no veo ninguna razón por la cual la macro que definió tiene que ser una macro; podría ser una función en su lugar. Puede hacer lo que quiera hacer si puede reescribir su macro como una función en su lugar.

+0

Muchas gracias @Brian. Tienes toda la razón. Pude hacer que funcionara como una función. Parece que todavía no he adquirido suficientes habilidades para jugar con macros. Pero dudo de aceptar tu respuesta. ¿No podría haber un caso teórico en el que alguien realmente necesite "reenviar" var-args de la función a la macro? La respuesta de @ Arthur con la función 'eval' puede ser más apropiada para esas personas. – Damien

Cuestiones relacionadas