Pattern matching as a tool for refactoring
Pattern matching is useful even if you don't end up using `match` because it helps you reason about your code

Table of Contents

1. A small example

Take the following snippet, slightly simplified from the original:

(defn reformat-req
  [{:keys [procid params] :as req}]
  (let [aq    (if (:id params) {:id (:id params)} {})
        query {:admin-data aq
               :tenant     (:tenant params)}
        q     {:type/procid   procid
               :type/time     (now)
               :type/tenant   (:tenant params)
               :type/user     (get-user req)
               :type/status   :processing
               :query/request query}]
    q))

Currently query looks like this:

;; when the :id key is in the params map
{:admin-data {:id id}
 :tenant     tenant}

;; when the :id key is NOT present
{:admin-data {}
 :tenant     tenant}

I need to refactor query to be like this:

{:type   :admin ; required
 :id     id     ; optional
 :tenant tenant ; optional
}

We know that the params map has at least the :tenant key, and it might have a :id key as well. Knowing this, we can identify 2 possible patterns that the data could be in:

  1. Contains both :tenant and :id
  2. Contains just :tenant

2. Pattern Matching to the Rescue

A perfect solution for this is pattern matching

(match params
  ;; 1. Contains both `:tenant` and `:id`
  {:tenant t :id d} {:type :admin :tenant t :id d}
  ;; 2. Contains just `:tenant`
  {:tenant t} {:type :admin :tenant t}
  ;; 3. Fall thru in case neither is present
  _ {:type :admin})

The match function from core.match first takes a map to match on, in this case params. Then, it takes a series of patterns and returns. In the first case, we want to match a map that has both :tenant and :id, and we want to extract the values for each key as t and d respectively. Then, we put that in a new map with :type :admin and return the entire thing to the caller. The second pattern, we only extract :tenant, and the third is a fail-safe in case the caller forgot to add :tenant for whatever reason (according to the spec, :tenant is technically optional). _ in this case is a wildcard.

Nota bene: we have to order our matches from most-specific to least-specific. For example if we put the {:tenant t} pattern first, then it could trigger a match even though there is a :id present.

3. A Simpler Way

Now you might look at this and say, "it sure looks like you are repeating yourself a lot." In which case you would be right, and there is a simpler way to write this code:

(merge {:type :admin} (select-keys params [:id :tenant]))

This does the same thing as the pattern matching. select-keys will take out :id and :tenant if they are present in params, and then we merge those keys with {:type :admin}. We can't use get in this case because, according to the spec, if the keys are in the final map, then they must not be nil, and get will return nil if it doesn't find the key in the params map.

However, it's very hard to see the succinct answer we came up with from the first code snippet. In this case, going through the exercise of pattern matching is valuable even though we didn't end up using match because it made the structure of the data explicit. In the original snippet, the data structure was there, but it was difficult to see clearly in all the moving parts.

4. The full refactor

At the end of the day, here is the full progression of refactoring:

;; original
(defn reformat-req
  [{:keys [procid params] :as req}]
  (let [aq    (if (:id params) {:id (:id params)} {})
        query {:admin-data aq
               :tenant     (:tenant params)}
        q     {:type/procid   procid
               :type/time     (now)
               :type/tenant   (:tenant params)
               :type/user     (get-user req)
               :type/status   :processing
               :query/request query}]
    q))

;; core.match version
(defn reformat-req
  [{:keys [procid params] :as req}]
  (let [query (match params
                {:tenant t :id d} {:type :admin :tenant t :id d}
                {:tenant t}       {:type :admin :tenant t}
                _                 {:type :admin})
        q     {:type/procid   procid
               :type/time     (now)
               :type/tenant   (:tenant params)
               :type/user     (get-user req)
               :type/status   :processing
               :query/request query}]
    q))

;; final, simplified
(defn reformat-req
  [{:keys [procid params] :as req}]
  (let [query (merge {:type :admin} (select-keys params [:id :tenant]))]
    {:type/procid   procid
     :type/time     (now)
     :type/tenant   (:tenant params)
     :type/user     (http-request->user req)
     :type/status   :processing
     :query/request query}))