我有一个带身体的宏:
(defmacro blah [& body] (dostuffwithbody))
但是我也想为它添加一个可选的关键字参数,所以在调用时它可能看起来像这样:
(blah :specialthingy 0 body morebody lotsofbody)
(blah body morebody lotsofboy)
我该怎么做?请注意,我正在使用Clojure 1.2,所以我也使用新的可选关键字参数解构。我天真地尝试这样做:
(defmacro blah [& {specialthingy :specialthingy} & body])
但显然这并没有很好的效果。我怎样才能完成这个或类似的东西?
答案 0 :(得分:9)
或许类似以下内容(另请参见defn
和clojure.core
中的其他一些宏的定义):
(defmacro blah [& maybe-option-and-body]
(let [has-option (= :specialthingy (first maybe-option-and-body))
option-val (if has-option (second maybe-option-and-body)) ; nil otherwise
body (if has-option (nnext maybe-option-and-body) maybe-option-and-body)]
...))
或者你可以更复杂;如果您认为您可能希望在某些时候有多种可能的选择,那么这可能是值得的:
(defn foo [& args]
(let [aps (partition-all 2 args)
[opts-and-vals ps] (split-with #(keyword? (first %)) aps)
options (into {} (map vec opts-and-vals))
positionals (reduce into [] ps)]
[options positionals]))
(foo :a 1 :b 2 3 4 5)
; => [{:a 1, :b 2} [3 4 5]]
答案 1 :(得分:3)
MichałMarczyk很好地回答了你的问题,但是如果你愿意接受(foo {}其余的args),那么一个带有可选关键字作为第一个参数的地图(在这个例子中为空),那么这个更简单代码可以正常工作:
(defn foo [{:keys [a b]} & rest] (list a b rest))
(foo {} 2 3)
=> (nil nil (2 3))
(foo {:a 1} 2 3)
=> (1 nil (2 3))
defmacro
的工作方式相同。这样做的好处是您不必记住可选关键字参数的特殊语法,并实现代码来处理特殊语法(也许其他人打算调用您的宏更习惯于指定键值对一张地图而不是外面的地图)。当然,缺点是您总是需要提供地图,即使您不想提供任何关键字参数。
当你想摆脱这个缺点时,一个小改进是检查你是否提供了一个地图作为你的参数列表的第一个元素:
(defn foo [& rest]
(letfn [(inner-foo [{:keys [a b]} & rest] (list a b rest))]
(if (map? (first rest))
(apply inner-foo rest)
(apply inner-foo {} rest))))
(foo 1 2 3)
=> (nil nil (1 2 3))
(foo {:a 2 :b 3} 4 5 6)
=> (2 3 (4 5 6))