Return a function so you don't repeat yourself

When designing a web client api the functions can end up looking similar. Everything boils down to the request execution. Each api function is just a bit different based on the exact http-method, api end point, or parameters. Removing this duplication might not be straightforward. A recent code review request had this situation. The author ended up writing a macro to extract the repetition.

I thought that by writing [a macro].. I wouldn't have to repeat myself for every function and instead let the macro do the code generation for me?

;;https://github.com/blmstrm/clj-spotify/blob/598f023110e617c2f791715cc31e4781da301e9a/src/clj_spotify/core.clj#L83

(defmacro def-spotify-api-call
  "Creates a function f with doc-string d that calls the http-verb verb for url url."
  [f verb url doc-string]
  `(defn ~f 
     ~doc-string
     ([m#] (~f m# nil))
     ([m# t#]
      (let [query-params# {(set-params-type ~verb) (modify-form-params m#) :oauth-token t# :content-type :json}]
        (->
          (try
            (~verb (replace-url-values m# ~url) query-params#)
            (catch Exception e# (ex-data e#)))
          (response-to-map))))))

;; Example usage, imagine 50 similar invocations
(def-spotify-api-call get-an-album client/get (str spotify-api-url "albums/id")
  " Takes two arguments, a map m with query parameters and an optional oauth-token t.
  Compulsory key in m is :id, optional key is :market.
  :id has to have the value of an existing album's id.
  :market is an ISO 3166-1 alpha-2 country code.
  Example: (get-an-album {:id \"0MnG7y5F1n4Wns63RjEItx\" :market \"SE\"} \"BQBw-JtC..._7GvA\")")

This macro uses its parameters in a straightforward manner. It only unquotes (~) them. No other code runs during the expansion. This is a great example to explore and see how a functional approach can replace it.

Separate code structure work from functional work

To start, it is important to separate the functional work from the syntax changes the macro does. The bulk of the work happens in the let block. The let extracts into a function named exec-request!.

(defn exec-request!
  [http-method url params oauth-token]
  (let [query-params {(set-params-type http-method) (modify-form-params params)
                      :oauth-token oauth-token
                      :content-type :json}]
    (->
      (try
        (http-method (replace-url-values params url) query-params)
        (catch Exception e (ex-data e)))
      (response-to-map))))

(defmacro def-spotify-api-call
  "Creates a function name with doc-string d that calls the http-method for url."
  [f verb url doc-string]
  `(defn ~f 
     ~doc-string
     ([m#] (~f m# nil))
     ([m# t#]
      (exec-request! ~verb ~url m# t#))))

This provides us a simpler macro to look at. Now the macro is only adding the syntax requirements for a multi-arity function. You might come to a standstill here, but functions can replace this too!

Function creation is functional

Function creation does not need a macro's syntax manipulation. Function creation can happen in normal functions. Refactoring and pulling it out of the macro requires multiple steps. First lets separate defn into its def and fn. One obstacle to this seperation is the recursion on ~f. Luckily fns in clojure take an optional name argument. This allows recursion by using this internal name. Lets call this one g#.

(defmacro def-spotify-api-call
  "Creates a function name with doc-string d that calls the http-method for url."
  [f verb url doc-string]
  `(def ~f 
     ~doc-string
     (fn g#
       ([m#] (g# m# nil))
       ([m# t#]
        (exec-request! ~verb ~url m# t#)))))

There is a problem will extracting the fn directly. It closes over the ~verb and the ~url. Those need to be parameters. But the function stored by def-spotify-api-call needs to take 1 or 2 parameters. Solving this problem requires adding another fn. It will take the additional two arguments and return the original fn.

(defmacro def-spotify-api-call
  "Creates a function name with doc-string d that calls the http-method for url."
  [f verb url doc-string]
  `(def ~f 
     ~doc-string
     ((fn [http-method# url#]
        (fn g#
          ([m#] (g# m# nil))
          ([m# t#]
            (exec-request! http-method# url# m# t#))))
      ~verb ~url)))

This provides a binding for the inner fn to close over http-method# and url#. Everything in the outer fn is local to it. Pulling it out allows for a name. Lets call it spotify-api-call.

(defn spotify-api-call
  [http-method url]
  (fn g
    ([params] (g params nil))
    ([params oauth-token]
      (exec-request! http-method url params oauth-token))))

(defmacro def-spotify-api-call
  "Creates a function name with doc-string d that calls the http-method for url."
  [f verb url doc-string]
  `(def ~f 
     ~doc-string
     (spotify-api-call ~verb ~url)))

spotify-api-call is a function constructor. Given the http-method and url it will return a function. Calling that function later with extra data will do the actual work.

Reviewing the macro

At this point def-spotify-api-call just shifts tokens around. Compare its macro invocation to the expansion.

(def-spotify-api-call get-an-album client/get (str spotify-api-url "albums/id")
  "doctring...")

(def get-an-album
  "docstring..."
  (spotify-api-call client/get (str spotify-api-url "albums/id")))

The macro no longer reduces code duplication. That was its purpose. Removing it makes sense.

Keeping it dry

The original code had a macro so the author "wouldn't have to repeat myself". The macro separated into code structure work vs functional work. A function constructor was able to return a multi-arity function definition. That reduced the macro to just token shifting, so it was removed. The end result keeps things dry with just functions.