Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure: Generating functions from template

Tags:

clojure

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.

like image 628
Konrad Garus Avatar asked Feb 17 '11 21:02

Konrad Garus


3 Answers

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

like image 99
mblinn Avatar answered Nov 14 '22 03:11

mblinn


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")
like image 2
Jürgen Hötzel Avatar answered Nov 14 '22 02:11

Jürgen Hötzel


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?

like image 1
Konrad Garus Avatar answered Nov 14 '22 03:11

Konrad Garus