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:
- Contains both
:tenant
and:id
- 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}))