How to Use Anonymous Functions in Clojure
What makes up defn
Named functions are defined with defn
, which is a combination of def
(creating variable) and fn
(creating function).
;; Function named 'do-something'
(def do-something
(fn [arg1 arg2]
(...)))
;; Equivalent, more concise way to write function
(defn do-something [arg1 arg2]
(...))
When a function has no name, i.e. no def
, it’s an anonymous function.
;; Function named "double"
(def double
(fn [x]
(* 2 x)))
;; Anonymous function
(fn [x] (* 2 x))
;; Invoke anonymous function
((fn [x] (* 2 x)) 10) ;; 20
Using anonymous functions with map and filter
When a function is called only once a name isn’t required. You’ll see this pattern when working with higher-order functions, such as map, filter, and reduce, where the anonymous functions aren’t reused elsewhere.
Let’s see how our doubling function would work with map
.
;; Structure for map function
(map function collection)
;; Function to apply to each item in collection
(fn [x] (* 2 x))
;; Collection to map through
[1 2 3 4 5]
;; Map through collection and double each element
(map (fn [x] (* 2 x)) [1 2 3 4 5]) ;; (2 4 6 8 10)
Shorthand for writing anonymous functions
When a function has only one argument, I prefer using the shorter form for anonymous functions: #()
.
Arguments aren’t in a list. Instead, they’re represented as:
%
for single argument%1, %2, %3, ...
for multiple arguments%&
for any remaining arguments
;; Single argument
(fn [x] (* 2 x))
#(* 2 %)
;; Multiple arguments
(fn [x y] (+ x y))
#(+ %1 %2)
;; Remaining arguments
(fn [x & args] (reduce + x args))
#(reduce + % %&)
When functions have more than two arguments, I’d recommend using the longer (fn)
form because it can be confusing to keep track of what each argument is.
Using anonymous functions with map and filter
In a previous example we’ve used map to double each number in a vector. Let’s work a more sophisticated collection!
;; Directory of all movies available for rent
(def movies
[{:id 1 :name "Hobbit" :price 2.50 :promoted true :genre :fantasy}
{:id 2 :name "Frozen" :price 6.00 :promoted false :genre :comedy}
{:id 3 :name "Wall-E" :price 3.50 :promoted true :genre :comedy}
{:id 4 :name "Avatar" :price 3.50 :promoted false :genre :sci-fi}
{:id 5 :name "Aliens" :price 1.00 :promoted true :genre :sci-fi}])
Getting values with map
When we’re getting values, map
creates a new sequence and movies
is not changed in any way.
;; Get only the ID and name of each movie.
(map (fn [movie] (select-keys movie [:id :name])) movies)
(map #(select-keys % [:id :name]) movies)
;; ({:id 1, :name "Hobbit"}
;; {:id 2, :name "Frozen"}
;; {:id 3, :name "Wall-E"}
;; {:id 4, :name "Avatar"}
;; {:id 5, :name "Aliens"})
Updating values with map
It’s misleading to say we’re updating values because movies
never gets changed. Instead we’re creating a new sequence with the updated values.
;; There's a 50% discount for renting movies. Return movies with updated prices.
(map (fn [movie] (merge movie {:price (* 0.5 (:price movie))})) movies)
(map #(merge % {:price (* 0.5 (:price %))}) movies)
;; ({:id 1, :name "Hobbit", :price 1.25, :promoted true, :genre :fantasy}
;; {:id 2, :name "Frozen", :price 3.0, :promoted false, :genre :comedy}
;; {:id 3, :name "Wall-E", :price 1.75, :promoted true, :genre :comedy}
;; {:id 4, :name "Avatar", :price 1.75, :promoted false, :genre :sci-fi}
;; {:id 5, :name "Aliens", :price 0.5, :promoted true, :genre :sci-fi})
Filtering values based on conditions
;; Get only movies that are promoted
(filter (fn [movie] (true? (:promoted movie))) movies)
(filter #(true? (:promoted %)) movies)
;; ({:id 1, :name "Hobbit", :price 2.5, :promoted true, :genre :fantasy}
;; {:id 3, :name "Wall-E", :price 3.5, :promoted true, :genre :comedy}
;; {:id 5, :name "Aliens", :price 1.0, :promoted true, :genre :sci-fi})
Filtering by one condition is easy enough. Let’s take a look how to do it with two!
;; Get only movies that are promoted AND cost less than $2
(filter (fn [movie] (and (true? (:promoted movie)) (< (:price movie) 2))) movies)
(filter #(and (true? (:promoted %)) (< (:price %) 2)) movies)
;; ({:id 5, :name "Aliens", :price 1.0, :promoted true, :genre :sci-fi})
Where to practice writing functions
When I started learning Clojure I had trouble switching back and forth between (fn)
and #()
. In the beginning I often made the mistake of writing my anonymous function as (#())
. This invokes the function.
Doing a lot of practice problems on 4clojure helped me get used to the syntax and practice core concepts. If you ever get stuck on a problem, I’ve shared my 4clojure solutions on Github.
Happy practicing!