Clojure: Refactoring From Thread Last (->>) To Thread First (->)
I use ->> (thread-last) and -> (thread-first) very often. When
I'm transforming data I find it easy to break things down mentally by
taking small, specific steps, and I find that -> & ->>
allow me to easily express my steps.
Let's begin with a (very contrived) example. Let's assume we have user
data and we need a list of all users in "new york", grouped by their
employer, and iff
their employer is "drw.com" then we only want their name - otherwise we
want all of the user's data. In terms of the input and the desired
output, below is what we have and what we're looking for.
;;; the data we will start out with -
(def jay {:name "jay fields" :employer "drw.com" :current-city "new york"})
(def john {:name "john dydo" :employer "drw.com" :current-city "new york"})
(def mike {:name "mike ward" :employer "drw.com" :current-city "chicago"})
(def chris {:name "chris george" :employer "thoughtworks.com" :current-city "new york"});;; the data we're looking for as a result
{"drw.com" ("jay fields" "john dydo"),
"thoughtworks.com" [{:name "chris george",
:current-city "new york",
:employer "thoughtworks.com"}]}
A solution that uses ->> can be found below.
(->> [jay john mike chris] (filter (comp (partial = "new york") :current-city)) (group-by :employer) (#(update-in % ["drw.com"] (partial map :name))))
The above example is very likely the first solution I would create. I go
about solving the problem step by step, and if the first step takes my
collection as the last argument then I will often begin by using
->>. However, after the solution is functional I will almost
always refactor to -> if any of my "steps" do not take the result of
the previous step as the last argument. I strongly dislike the above
solution - using an anonymous function to make update-in usable with a
thread-last feels wrong and is harder for me to parse (when compared
with the alternatives found below).
The above solution could be refactored to the following solution
(-> [jay john mike chris]
(->> (filter (comp (partial = "new york") :current-city))
(group-by :employer))
(update-in ["drw.com"] (partial map :name)))This solution is dry, but it also groups two of my three steps together,
while leaving the other step at another level. I expect many people to
prefer this solution, but it's not the one that I like the best.
The following solution is how I like to refactor from ->> to ->
(-> [jay john mike chris]
(->> (filter (comp (partial = "new york") :current-city)))
(->> (group-by :employer))
(update-in ["drw.com"] (partial map :name)))
My preferred solution has an "extra" thread-last, but it allows me to keep everything on the same level. By keeping everything on the same level, I'm able to easily look at the code and reason about what it's doing. I know that each step is an isolated transformation and I feel freed from keeping a mental stack of what's going on in the other steps.
(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)





