Being a functional language, functions in Clojure are first-class citizens. This means that they can be passed to and generated by other functions just like data. Clojure in particular comes with a rich set of high-order functions that derive new functions based on existing ones. We will explore here four important cases: partial, comp, memoize and juxt. These function-generating functions fall into a broader category of higher-order functions, such as map, reduce, apply, complement, to name a few, which operate on existing functions.
In general, given an existing function my-function, with parameters p1, p2, ..., pN we can fix some of its parameters p1, p2, ..., pM to have constant values v1, v2, ..., vM. We do so by applying partial as follows: (partial my-function v1 v2 ... vM).
The result is a new function which applies the original one my-function by fixing p1=v1, p2=v2, ..., pM=vM. Our new function takes as input the remaining parameters pM+1, pM+2, ..., pN, whose values have not been indicated when applying partial:
(def my-new-function
(partial my-function v1 v2 ... vM))
(my-new-function xM+1 xM+2 ... xN)
; => equivalent to (my-function v1 v2 ... vM xM+1 xM+2 ... xN)
As a simple example, let's define a function inc-by-9 which increases by 9, a fixed amount, whatever we pass-in. We could implement such function as follows:
(def inc-by-9 (partial + 9))
(inc-by-9 5)
; => 14
As a second example, we have a function generic-greetings that uses the name of the person we wish to greet along with initial and final text messages:
(defn generic-greetings
[initial-text final-text name]
(println (str initial-text name final-text)))
And use partial to always use a specific greetings message:
(def say-hello-and-how-are-you-doing
(partial generic-greetings "Hello " ", how are you doing?"))
(say-hello-and-how-are-you-doing "Mary")
; => Hello Mary, how are you doing?
comp can be used to create a composition of any number of functions we want to compose. In general, composing N functions f1, f2, ..., fN means applying those functions in sequential order, passing the ouput of the previous function to the next one. In mathematical notation this is expressed as:
f1 (f2 (... fN(x)))
In clojure, this is equivalent to doing:
(f1 (f2 (... (fN x))))
By using comp, we can create a new function which performs this composition for us:
(def my-function-composition
(comp f1 f2 ... fN))
(my-function-composition x)
; equivalent to
(f1 (f2 (... (fN x))))
As an example, let's say we want to sum a series of numbers and then multiply the result by 6. We can do so as follows:
(def six-times-result-sum
(comp (partial * 6) +))
(six-times-result-sum 3 2)
; = ((partial * 6) (+ 3 2))
; = (* 6 (+ 3 2))
; = 30
memoize allows to reuse previously computed results associated with a given input. Given the same input, the memoized function returns the same result without having to recompute it again. It takes advantage of the fact that clojure functions are, by default, referentially transparent: given the same input, they always produce the same output, which makes it possible to reuse previous computations with ease.
In order to see how this works, let us use a synthetic case where the function sleeps for two seconds before producing the output, so we can easily compare the computation time before and after using memoize.
(defn my-time-consuming-fn
"Original, time-consuming function"
[x]
(Thread/sleep 2000)
(* x 2)
)
; We measure the time it takes the original function
(time (my-time-consuming-fn 3))
; => "Elapsed time: 2007.785622 msecs"
; => 6
; We define a memoized function
(def my-memoized-fn
(memoize my-time-consuming-fn) )
; We call the memoized function and measure its execution time.
; The first execution actually runs the function, taking
; similar time as the original.
(time (my-memoized-fn 3))
; => "Elapsed time: 2001.364052 msecs"
; => 6
; Subsequent calls reuse the previous computation, taking less
; time. Time is further reduced in additional calls.
(time (my-memoized-fn 3))
; => "Elapsed time: 1.190291 msecs"
; => 6
(time (my-memoized-fn 3))
; => "Elapsed time: 0.043701 msecs"
; => 6
; Changing the input makes the function be executed, so that
; we observe a similar computation time as the original
(time (my-memoized-fn 4))
; => "Elapsed time: 2000.29306 msecs"
; => 8
; Again, repeating the same input will make the
; execution be skipped, and the associated output returned
(time (my-memoized-fn 4))
; => "Elapsed time: 0.043701 msecs"
; => 8
juxt generates a function that applies, in left to right order, the functions passed to it. The result is a vector with the individual results of each function as components of the vector:
((juxt f g h) x) ; => [(f x) (g x) (h x)]
As a simple example, we generate a function that computes the product of x by successive factors, from 2 to 5:
((juxt (partial * 2) (partial * 3) (partial * 4) (partial * 5)) 10) ; => [20 30 40 50]