How to generate functions with macros

Mar 3, 2016

How to generate functions with macros

This blog post will show you how to use macros to write functions with similar
behavior. This general method will greatly reduce the amount of code that
you need to type and will keep your codebase clean and easy to test.

Let’s assume for a moment that you are building a system where you have users.
A user has several fields including name, email, phone-number and
gender. For the purposes of this article we will model a user as a simple map.

The user data structure.

Requirements

At some point in time, your boss comes into your office and says:
“Jim, we need to be able to find users in the database by name, email, phone
number and gender and we need it now!”

You are an awesome clojure developer so you quickly come up with a straight
forward solution:

(defnfind-by-id[id](->>(build-query"SELECT * FROM users WHERE id=%s"id);; creates an SQL query
(run-query);; runs the query against the database
(rows->maps)));; Iterate over the cursor and parse the rows to maps.
(defnfind-by-email[email](->>(build-query"SELECT * FROM users WHERE id=%s"email);; creates an SQL query
(run-query);; runs the query against the database
(rows->maps)));; Iterate over the cursor and parse the rows to maps.
;;...

At this point you realize that you are repeating yourself and since you are an
awesome developer you start to think how you can do this better.

Keeping it DRY with functions

You immediately realize: well what if I had a function find-by-field which
finds a user in the database given a field and a value for that field.

(defnfind-by-field[fieldvalue](->>(build-query"SELECT * FROM users WHERE %s=%s"fieldvalue);; creates an SQL query
(run-query);; runs the query against the database
(rows->maps)));; Iterate over the cursor and parse the rows to maps.
(defnfind-by-email[email]"Finds and returns a list of users in the database with the given email"(find-by-email"email"email)(defnfind-by-gender[gender]"Finds and returns a list of users in the database with the given gender"(find-by-field"gender"gender))(defnfind-by-phone-number[phone-number]"Finds and returns a list of users in the database with the given phone-number."(find-by-field"phone_number"phone-number))(defnfind-by-name[name]"Finds and returns a list of users in the database with the given name."(find-by-field"name"name))

You look at your work and think, damn, that’s a good simple, clean solution.
But the question remains, can we do better? Yes we can! Looking at your code
you quickly realize that the find-by-{field} functions can be re-writter
with macros which will remove all the needless boiler plate and code repetition.

It’s macro time!

As always when describing macros I like to first see how the macro invocation
will look like as well as the expanded code and then it’s usually easier to
implement the macro.

(defuser-finders:name:gender:phone-number:email);; Will expand to
(do(defnfind-by-name[name](find-by-value"name"name))(defnfind-by-gender[gender](find-by-gender"gender"gender));;...etc...

The cool think about this approach is that it actually defines good ol’
functions which can be composed and used as you would normally use
functions anywhere else in your awesome clojure codebase.

So without further delay, here is the code: A 20-liner can be used to
define as many finder functions as possible.

(defn-field->db-field-name"Given a keyword like :phone-number, returns the database equivalent
e.g. 'phone_number' as a string."[field-kw](.replaceAll(namefield-kw)"-""_"))(defn-field->finder-name"Given a keyword like :phone-number, returns a finder function name
as a symbol e.g. find-by-phone-number"[field-kw](symbol(str"find-by-"(namefield-kw))))(defmacrodefuser-finders[&fields](cons'do(map(fn[field]`(defn~(field->finder-namefield)~(str"Finds and returns a list of users in the database ""with the given "(namefield)".")[arg#](find-by-field~(field->db-field-namefield)arg#)))fields)))

Wrapping it up

So there you go, a simple extensible approach with 0 performance degradation
that will make your code clean and extremely easy to extend. The key points to
note about the macro approach:

Adding another finder function is as easy as adding another argument to the
defuser-finders macro invocation

The generated code is as performant as hand written code. Since macro
expansion happens before runtime, these functions will actuallty be compiled
into bytecode.

The generated code comes with documentation so users of your code can
(doc find-by-user).

I hope you liked this post. As always critticism and comments are more than
welcome. Cheers and happy coding!