En clojure, comment appliquer une macro à une liste?
Dans clojure
, apply
ne peut pas être appliqué à une macro. Par exemple, (apply and [true false])
déclenche une exception. Je pensais à la solution de contournement suivante:
(defmacro apply-macro[func args] `(~func ~@args))
À première vue, cela semblait plutôt bien fonctionner:
(apply-macro and [true 5]); 5
(apply-macro and [true 5 0]); 0
(let [a 0] (apply-macro and [true a])); 0
Mais, quand je lui ai passé une variable qui pointe vers un vecteur, elle s'est effondrée.
(let [a [true]] (apply-macro and a)); java.lang.IllegalArgumentException:
;Don't know how to create ISeq from: clojure.lang.Symbol
Quelle déception!!!!
Une idée de comment réparer apply-macro
?
5 réponses
Le problème est que a
est juste un symbole au moment de la compilation. Il n'y a donc aucun moyen pour une macro à la compilation de voir ce qu'elle contient et de faire l'expansion nécessaire. Par conséquent, vous devez développer la macro au moment de l'exécution en utilisant eval.
Une façon de le faire est simplement d'envelopper la macro dans une fonction qui appelle eval, ce qui peut être fait avec cette macro pratique "fonctionnaliser":
(defmacro functionize [macro]
`(fn [& args#] (eval (cons '~macro args#))))
(let [a [true]] (apply (functionize and) a))
=> true
Si vous le souhaitez, vous pouvez également définir apply-macro en termes de fonctionnaliser:
(defmacro apply-macro [macro args]
`(apply (functionize ~macro) ~args))
(let [a [true false]] (apply-macro and a))
=> false
Ayant dit tout cela, je pense toujours que la meilleure chose à faire est d'éviter complètement les macros quand elles ne sont pas vraiment nécessaires: elles ajoutent une complexité supplémentaire et sont mieux réservées aux cas où vous avez vraiment besoin de génération de code de compilation. Dans ce cas, vous ne le faites pas: la réponse D'Alex Taggart donne un bon exemple de la façon d'atteindre un objectif similaire sans macros, ce qui est probablement plus approprié dans la plupart des situations.
Vous ne le faites pas.
Les Macros sont développées pendant l'évaluation / la compilation, pas au moment de l'exécution, donc les seules informations qu'elles peuvent utiliser sont les args transmis, mais pas ce que les args évaluent à l'exécution. C'est pourquoi un vecteur littéral fonctionne, car ce vecteur littéral est là au moment de la compilation, mais a
n'est qu'un symbole; il n'évaluera un vecteur qu'au moment de l'exécution.
Pour avoir un comportement similaire à and
pour les listes, Utilisez (every? identity coll)
.
Pour avoir un comportement similaire à or
pour les listes, Utilisez (some identity coll)
.
Bien sûr, la bonne réponse est Ne faites pas cela . Mais, puisque je ne peux pas résister à un bon hack:
(defmacro apply-macro
"Applies macro to the argument list formed by prepending intervening
arguments to args."
{:arglists '([macro args]
[macro x args]
[macro x y args]
[macro x y z args]
[macro a b c d & args])}
[macro & args+rest]
(let [args (butlast args+rest)
rest-args (eval (last args+rest))]
`(eval
(apply (deref (var ~macro))
'(~macro ~@args ~@rest-args)
nil
~@(map #(list 'quote %) args)
'~rest-args))))
Utilisation:
hackery> (->> (range 5) rest rest rest rest)
(4)
hackery> (apply-macro ->> (range 5) (repeat 4 'rest))
(4)
Qualifications:
- la macro ne doit pas être citée, et les arguments intermédiaires sont passés sans évaluation à la macro. Cependant, l'argument" rest " est évalué, et doit évaluer à une liste de symboles ou de formes, dont chacun sera passé sans évaluation à la macro.
- cela ne fonctionnera pas avec les macros qui utilisent l'
&env
argument.
Cette approche ne fonctionne pas si la liste d'arguments peut avoir une longueur illimitée , mais si vous devez seulement appliquer aux listes jusqu'à la longueur n
vous pouvez créer une fonction wrapper avec n
arities:
user> (defmacro foo [& rest] `(println ~@rest))
#'user/foo
user> (apply foo [1 2])
CompilerException java.lang.RuntimeException: Can't take value of a macro: #'user/foo, compiling:(*cider-repl repo*:865:7)
user> (defn foo-up-to-ten-args
([a] (foo a))
([a b] (foo a b))
([a b c] (foo a b c))
([a b c d] (foo a b c d))
([a b c d e] (foo a b c d e))
([a b c d e f] (foo a b c d e f))
([a b c d e f g] (foo a b c d e f g))
([a b c d e f g h] (foo a b c d e f g h))
([a b c d e f g h i] (foo a b c d e f g h i))
([a b c d e f g h i j] (foo a b c d e f g h i j)))
#'user/foo-up-to-ten-args
user> (apply foo-up-to-ten-args [1 2])
1 2
nil
user> (apply foo-up-to-ten-args (range 0 10))
0 1 2 3 4 5 6 7 8 9
nil
user> (apply foo-up-to-ten-args (range 0 11))
ArityException Wrong number of args (11) passed to: user/foo-up-to-ten-args clojure.lang.AFn.throwArity (AFn.java:429)
Dans mon cas, cela a fait ce dont j'avais besoin sans eval
.
Lors de l'application d'arguments pour une macros conditionnelles telles que or
, case
, cond
& condp
par exemple. Vous pouvez utiliser l'some
, every
& partition
fonctions. La clause else unique est laissée de côté dans ces exemples mais peut être ajoutée assez facilement
;apply to the 'or' macro
(some identity [nil false 1 2 3])
=> 1
;apply to the 'case' macro.
(some
(fn [[case value]]
(and (= case 2) value))
(partition 2 [1 "one" 2 "two" 3 "three"]))
=> "two"
;apply to the 'cond' macro
(some
(fn [[case value]]
(and case value))
(partition 2 [false "one" true "two" false "three" :else "four"]))
=> "two"
;apply to the 'condp' macro
(let [[f v & args] [= 2 1 "one" 2 "two" 3 "three"]]
(some
(fn [[case value]]
(and (f case v) value))
(partition 2 args)))
every?
peut être utilisé pour la and
macro
;apply to the 'and' macro
(every? identity [true true true])
=> true