Clojure: Generating functions from template
I have the following code for a generic conversion library:
(defn using-format [format] {:format format})
(defn- parse-date [str format]
(.parse (java.text.SimpleDateFormat. format) str))
(defn string-to-date
([str]
(string-to-date str (using-format "yyyy-MM-dd")))
([str conversion-params]
(parse-date str (:format (merge (using-format "yyyy-MM-dd") conversion-params)))))
I need to be able to call it like this:
(string-to-date "2011-02-17")
(string-to-date "2/17/2011" (using-format "M/d/yyyy"))
(string-to-date "2/17/2011" {})
The third case is somewhat problematic: the map does not necessarily contain the key :format
which is critical for the function. That's why the merge
with default value.
I need to have a dozen of similar functions for conversions between all other types. Is there a more elegant way that would not require开发者_开发知识库 me to copy-paste, use merge
etc. in every single function?
Ideally, looking for something like this (macro?):
(defn string-to-date
(wrap
(fn [str conversion-params]
(parse-date str (:format conversion-params))) ; implementation
{:format "yyyy-MM-dd"})) ; default conversion-params
... that would produce an overloaded function (unary and binary), with binary having a merge
like in the first example.
So to define this a little more strictly, you want to create a macro that creates converter functions. A converter function is a function with two arities, one argument and two arguments. The first argument to a converter function is the object to be converted. The second argument is a map of options, that will somehow affect the conversion (like a format string in your example.)
A default parameter map can be specified. When called with one argument, a converter function will use the default parameter map. When called with two arguments, a converter function will merge the default parameter map with the passed in parameter map, such that the passed in parameters override the defaults if they exist.
Let's call this macro def-converter. Def converter will take three arguments, the first is the name of the function to be created. The second is an anonymous function of two arguments that implements the two-arity converter, without the default parm merging. The third argument is the default parm map.
Something like this will work:
(defmacro def-converter [converter-name converter-fn default-params]
(defn ~converter-name
([to-convert#]
(let [default-params# ~(eval default-params)]
(~converter-fn to-convert# default-params#)))
([to-convert# params#]
(let [default-params# ~(eval default-params)]
(~converter-fn to-convert# (merge default-params# params#))))))
Then you can use it like:
(def-converter
string-to-date
(fn [to-convert conversion-params]
(parse-date to-convert conversion-params))
(using-format "yyyy-MM-dd"))
But you have to make a change to one of your helper functions:
(defn- parse-date [str params]
(.parse (java.text.SimpleDateFormat. (:format params)) str))
This is because the macro needs to be general enough to handle arbitrary maps of parameters, so we can't count on. There are probably ways around it, but I can't think of one offhand that's not messier than just pushing that off onto a helper function (or the anonymous function that needs to be passed into def-converter).
clojure.contrib.def/defnk is handy if you need functions with default keyword arguments:
(use 'clojure.contrib.def)
...
(defnk string-to-date [str :format "yyyy-MM-dd"]
(parse-date str format))
(string-to-date "2011-02-17")
(string-to-date "2/17/2011" :format "M/d/yyyy")
For the record, here's what I figured out later at night:
(defmacro defconvert [name f default]
`(defn ~name
([v#] (~name v# ~default))
([v# conversion-params#] (~f v# (merge ~default conversion-params#)))))
It seems to work and generate exactly the definition I had up there. I it possible with defnk
or some other built-in mechanism, having a map of default values and accepting override of some but not necessarily all?
精彩评论