A quick tip which helped me out in a few situations. I’d be inclined to
point people to core.match for
any matching needs, but the fact that it doesn’t play well with
clojure’s ahead-of-time (AOT
) compilation requires playing dirty
dynamic namespace loading tricks to use it.
A common case I stumbled upon is having a list of homogenous records - say, coming from a database, or an event stream - and needing to take specific action based on the value of several keys in the records.
Take for instance an event stream which would contain homogenous records of with the following structure:
[{:user "bob"
:action :create-ticket
:status :success
:message "succeeded"
:timestamp #inst "2013-05-23T18:19:39.623-00:00"}
{:user "bob"
:action :update-ticket
:status :failure
:message "insufficient rights"
:timestamp #inst "2013-05-23T18:19:40.623-00:00"}
{:user "bob"
:action :delete-ticket
:status :success
:message "succeeded"
:timestamp #inst "2013-05-23T18:19:41.623-00:00"}]
Now, say you need do do a simple thing based on the output of the value
of both :action
and :status
.
The first reflex would be to do this within a for
or doseq
:
(for [{:keys [action status] :as event}]
(cond
(and (= action :create-ticket) (= status :success)) (handle-cond-1 event)
(and (= action :update-ticket) (= status :success)) (handle-cond-2 event)
(and (= action :delete-ticket) (= status :failure)) (handle-cond-3 event)))
This is a bit cumbersome. A first step would be to use the fact that
clojure seqs and maps can be matched, by narrowing down the initial
event to the matchable content. juxt
can help in this situation, here
is its doc for reference.
Takes a set of functions and returns a fn that is the juxtaposition of those fns. The returned fn takes a variable number of args, and returns a vector containing the result of applying each fn to the args (left-to-right).
I suggest you play around with juxt
on the repl to get comfortable
with it, here is the example usage we’re interested in:
(let [narrow-keys (juxt :action :status)]
(narrow-keys {:user "bob"
:action :update-ticket
:status :failure
:message "insufficient rights"
:timestamp #inst "2013-05-23T18:19:40.623-00:00"}))
=> [:update-ticket :failure]
Given that function, we can now rewrite our condition handling code in a much more succint way:
(let [narrow-keys (juxt :action :status)]
(for [event events]
(case (narrow-keys event)
[:create-ticket :success] (handle-cond-1 event)
[:update-ticket :failure] (handle-cond-2 event)
[:delete-ticket :success] (handle-cond-3 event))))
Now with this method, we have a perfect candidate for a multimethod:
(defmulti handle-event (juxt :action :status))
(defmethod handle-event [:create-ticket :success]
[event]
...)
(defmethod handle-event [:update-ticket :failure]
[event]
...)
(defmethod handle-event [:delete-ticket :success]
[event]
...)
Of course, for more complex cases and wildcard handling, I suggest taking a look at core.match.