A couple of days ago, I finally had enough of manually extracting
function keyword arguments. defnk is cool and all, but it does nothing
for fn, letfn, defmethod, or any other form with a parameter list. Map
destructuring is also cool, but providing defaults requires writing
each symbol twice, once in :keys and once in :or.

The macro I ended up with, fn-keywords, owes much to Common Lisp's
lambda lists. Instead of an outer wrapper for Clojure's various
function forms, it provides a kind of inner wrapper, really a let form
specialized for keyword arguments. Each keyword is defined in a
keyword spec, which allows for an optional default value, and for an
optional symbol to be bound to true if the keyword was actually
provided in the function call.

Note that keywords arguments may be & rest in style, or provided as
either maps or vectors. This should cover all useful cases for keyword
arguments, including their use in functions with multiple arities.


Example:

(defn test1 [req1 req2 & kw-args]
  (fn-keywords [:kw1
                [:kw2 "kw2 default"]
                [:kw3 "kw3 default" kw3-supplied?]]
      kw-args
    ;; function body
    {:req1 req1 :req2 req2 :kw1 kw1 :kw2 kw2
     :kw3 kw3 :kw3-supplied? kw3-supplied?}))

> (test1 1 2 :kw3 "there")
{:req1 1, :req2 2, :kw1 nil, :kw2 "kw2 default",
 :kw3 "there", :kw3-supplied? true}
> (test1 1 2 :kw1 "there")
{:req1 1, :req2 2, :kw1 "there", :kw2 "kw2 default",
 :kw3 "kw3 default", :kw3-supplied? false}
> (test1 1 2 :kw1 "hello" :kw2 "there")
{:req1 1, :req2 2, :kw1 "hello", :kw2 "there",
 :kw3 "kw3 default", :kw3-supplied? false}


Macro code:

(defmacro fn-keywords
  "Adds flexible keyword handling to any form which has a parameter
list: fn,
   defn, defmethod, letfn, and others. Keywords may be passed to the
surrounding
   form as & rest arguments, lists, or maps. Lists or maps must be
used for
   functions with multiple arities if more than one arity has keyword
   parameters. Keywords are bound inside fn-keywords as symbols, with
default
   values either specified in the keyword spec or nil. Keyword specs
may consist
   of just the bare keyword, which defaults to nil, or may have the
general form
   [:keyword-name keyword-default-value* keyword-bound?*]. keyword-
bound? is an
   optional symbol bound to true if the keyword was supplied, and to
false
   otherwise."
  [kw-spec-raw kw-args & body]
  (let [kw-spec  (map #(if (sequential? %) % [%]) kw-spec-raw)
        keywords (map first kw-spec)
        symbols  (map (comp symbol name) keywords)
        defaults (map second kw-spec)
        destrmap {:keys (vec symbols)
                  :or (apply hash-map (interleave symbols defaults))}
        supplied (reduce (fn [m [k v]] (assoc m k v)) (sorted-map)
                         (remove (fn [[_ val]] (nil? val))
                                 (partition 2
                                            (interleave keywords
                                                        (map (comp
second rest)
                                                             kw-
spec)))))
        kw-args-map (gensym)]
    `(let [kw-args# ~kw-args
           ~kw-args-map (if (map? kw-args#) kw-args# (apply hash-map
kw-args#))
           ~destrmap ~kw-args-map]
       ~@(if (empty? supplied)
             body
             `((apply (fn [~@(vals supplied)]
                        ~...@body)
                      (map (fn [x#] (contains? ~kw-args-map x#))
                           [~@(keys supplied)])))))))


More examples:

(defmulti test2 (fn [x & rest] (class x)))

(defmethod test2 java.lang.Integer [req & kw-args]
  (fn-keywords [[:kw1 "default" kw1-supplied?]] kw-args
    {:method "integer"
     :req req
     :kw1 kw1
     :kw1-supplied? kw1-supplied?}))

(defmethod test2 java.lang.String [req & kw-args]
  (fn-keywords [[:kw1 "default"]] kw-args
    {:method "string"
     :req req
     :kw1 kw1}))

(defn test3
  ([req1 kw-args]
     (fn-keywords [:kw1 :kw2] kw-args
       {:req1 req1 :kw1 kw1 :kw2 kw2}))
  ([req1 req2 kw-args]
     (fn-keywords [:kw1 :kw2] kw-args
       {:req1 req1 :req2 req2 :kw1 kw1 :kw2 kw2})))

(defn test4 []
  (letfn [(inner [& kw-args]
            (fn-keywords [:kw1 :kw2] kw-args
              {:kw1 kw1 :kw2 kw2}))]
    (inner :kw1 "hello" :kw2 "world")))


Credit where it's due: I lifted ideas and bits of code from defnk.
Thanks, Meikel!

Comments welcome.

--
Constantine Vetoshev

-- 
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en

Reply via email to