1

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 Answer 1

3

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

Sign up to request clarification or add additional context in comments.

1 Comment

That is fantastic, Michal! I appreciate it. You taught me a lot in the process too.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.