Using `reduce` to Implement Other Clojure Functions
In
yesterday’s
post, we took a look at reduce
, one of the true workhorses of
functional programming. In today’s post, we’ll see how we can use
reduce
to implement some other functions in the Clojure standard
library.
As we learned previously, reduce
takes three arguments: a reducing
function, an optional starting value, and a collection. If you
don’t supply reduce
with a starting value, it will use the first
item in the collection as the starting value and start processing from
the second item.
We also learned that a reducing function takes two arguments. The
first argument is the current state of the reduction, and the second
is the input value to be used to compute the new state, which is the
return value. In yesterday’s post, I said that there are other Clojure
standard library functions that can act as a reducing function. One of
the most important is conj
.
Often, we think of conj
as just adding an item to a vector, like so
user> (conj [1 2] 3)
[1 2 3]
But conj
also works on other data types such as maps and sets.
user> (conj {:a 1} [:b 2])
{:a 1, :b 2}
user> (conj #{:a :b} :c)
#{:c :b :a}
It turns out that conj
is also a reducing function. It takes a
collection (the state) as the first argument and an item to add to the
collection (the input) as its second argument.
We can use reduce
with conj
as our reducing function to create our
own Clojure Crazy version of into
.
user> (defn cc-into [to from]
(reduce conj to from))
#'user/cc-into
user> (cc-into {:a 1 :b 2} [[:c 3]])
{:a 1, :b 2, :c 3}
user> (cc-into #{} [1 2 3 4 5 6])
#{1 4 6 3 2 5}
It’s fairly easy to re-implement other Clojure standard functions,
too. Here’s a re-implementation of filterv
, which is basically just
a non-lazy version of filter
which returns its results in a vector
instead of a lazy sequence.
user> (filterv odd? (range 10))
[1 3 5 7 9]
user> (defn cc-filterv [pred coll]
(reduce (fn [state input]
(if (pred input)
(conj state input)
state))
[]
coll))
#'user/cc-filterv
user> (cc-filterv odd? (range 10))
[1 3 5 7 9]
Here, our reducing function applies the predicate to the input value
and if it returns “truthy,” the reducing function adds the input to
the state via conj
. If the predicate returns “falsey” (either false
or nil
), then the reducing function returns the state
unchanged. This has the effect of not including that particular input
item in the output vector.
Finally, here’s a re-implementation of mapv
.
user> (defn cc-mapv [f coll]
(reduce (fn [state input]
(conj state (f input)))
[]
coll))
#'user/cc-mapv
user> (cc-mapv (partial * 5) (range 10))
[0 5 10 15 20 25 30 35 40 45]
It’s amazingly simple. Our reducing function applies f
to every
input
and then adds the result to the state
using conj
. That’s
it.
We could go on and re-implement similar replacements for lots of other
collection functions such as distinct
, group-by
, keep
, remove
,
replace
, etc. They’re all variations on this. If you want some
exercises to help these ideas really stick, try implementing them all
yourself. Start with keep
and remove
. Then, move on to distinct
,
group-by
, and replace
.
It’s important to note that all of our replacement functions have been
“greedy” like mapv
and filterv
, not “lazy” like map
or
filter
. According to its nature, reduce
processes the full input
collection before it returns a result.
Also, we’ve used conj
to build the output vector since it adds items
to the back of the vector, not the front like cons
. Thus, it
preserves the order of the items in the collection as it processes
them. If you implement a replacement for group-by
, you’ll need to
think about using functions other than conj
to build up the output
value.
Next time, we’ll see how reduce
and these ideas about
re-implementing functions like filterv
and mapv
using reduce
start to set the stage for understanding Clojure transducers.