1
votes

I have a bit of a dilemma here that I can't figure out. I'm trying to create a bunch of functions that are all pretty similar except for a couple things (included the number of arguments they take).

I wrote a macro to create one of these functions that seems to be working correctly. Here's the macro

(defmacro gen-fn
  [meth args transform-fns]
  `(fn [~'conn ~@args]
     (->> (. metadata-service
            ~meth
            (sec/single-use-ticket ~'conn)
            ~@args)
          ~@transform-fns)))

So I can create two functions

(def get-source (gen-fn getSource [version source] [bean]))
(def get-all-sources (gen-fn getAllSources [version] [(map bean)]))

and both work correctly when I call them like this:

(get-source conn "2013AB" "WHO97")
(get-all-sources conn "2013AB")

Now I have about 600 of these functions to create so it would be nice if I could simplify this a bit (and eventually maybe read it in from an external source at application startup). So my first thought here was to construct a map like this:

(def metadata-methods
  { "getSources" [["version" "source"] ["bean"]]
    "getAllSources" [["version"] ["(map bean)"]] })

or something along those lines then use doseq or something like it to create the functions

(doseq [[function-label [args transform-fns]] metadata-methods]
  (intern *ns* (symbol (->kebab-case function-label))
               (gen-fn function-label [version source] [bean])))

When I run this it seems to work, but calling (get-source conn "2013AB" "WHO97") throws an exception saying there is no matching method "function_label" for class Proxy...

So somehow the macro isn't creating the function correctly.

So my questions are 1) Is there an easy way to make this work? 2) Am I making something more complicated than it needs to be? Is there an easier way to accomplish the same thing?

A plain function would work except for the fact that each of the functions to be generated takes a different number of arguments and I really would like each of the functions to have a fixed arity.

1

1 Answers

3
votes

Macros are passed as arguments the actual argument expressions in the macro call, so in this call:

(gen-fn function-label [version source] [bean])

gen-fn will be passed the actual symbol function-label, a vector of two symbols and a vector of one symbol as arguments. So this is why get-source doesn't work with the doseq approach.

The usual way to accomplish this sort of thing is to define a macro like your gen-fn and another macro, say def-fns (following the usual pattern of using a pluralized version of the original macro's name and changing gen to def because we're creating Vars), to emit multiple gen-fn forms wrapped in a do:

(defmacro def-fns [& args]
  `(do ~@(for [[meth args transform-fns] (partition 3 args)
               :let [name (symbol (->kebab-case (str meth)))]]
           `(def ~name (gen-fn ~meth ~args ~transform-fns)))))

Then say

(def-fns
  getSource [version source] [bean]
  getAllSources [version] [(map bean)])

If you'd rather use a map, that's possible too:

;; def the map first
(defmacro def-fns []
  `(do ~@(for [[meth [args transform-fn]] metadata-methods
               :let [name (symbol (->kebab-case meth))]
           `(def ~name (gen-fn ~meth ~args ~transform-fn)))))

Note that the macro function will use the compile-time value of metadata-methods (which is fine).