views:

101

answers:

2

I have a macro that takes a body:

(defmacro blah [& body] (dostuffwithbody))

But I'd like to add an optional keyword argument to it as well, so when called it could look like either of these:

(blah :specialthingy 0 body morebody lotsofbody)
(blah body morebody lotsofboy)

How can I do that? Note that I'm using Clojure 1.2, so I'm also using the new optional keyword argument destructuring stuff. I naively tried to do this:

(defmacro blah [& {specialthingy :specialthingy} & body])

But obviously that didn't work out well. How can I accomplish this or something similar?

+2  A: 

Michał Marczyk answered your question nicely, but if you're willing to accept (foo {} rest of the args), so a map with the optional keywords as the first argument (empty in this example), then this more simple code will work fine:

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

Works the same for defmacro. The advantage of this is that you don't have to remember a special syntax for optional keyword parameters, and implement code to handle the special syntax (maybe other people who are going to call your macro are more used to specifying key-value pairs in a map than outside of it). The disadvantage is of course that you always have to provide a map, even when you don't want to provide any keyword arguments.

A small improvement when you want to get rid of this disadvantage is checking if you provided a map as the first element of your argument list:

(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))
Michiel Borkent
+3  A: 

Something like the following perhaps (also see how defn and some other macros in clojure.core are defined):

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

Alternatively you could be more sophisticated; it might be worthwhile if you think you might want to have multiple possible options at some point:

(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]]
Michał Marczyk