Here's an elegent (in my opinion) definition of comp:
(defn comp [& fs]
(reduce (fn [result f]
(fn [& args]
(result (apply f args))))
identity
fs))
The nested anonymous functions might make it hard to read at first, so let's try to address that by pulling them out and giving them a name.
(defn chain [f g]
(fn [& args]
(f (apply g args))))
This function chain is just like comp except that it only accepts two arguments.
((chain inc inc) 1) ;=> 3
((chain rest reverse) [1 2 3 4]) ;=> (3 2 1)
((chain inc inc inc) 1) ;=> ArityException
The definition of comp atop chain is very simple and helps isolate what reduce is bringing to the show.
(defn comp [& fs]
(reduce chain identity fs))
It chains together the first two functions, the result of which is a function. It then chains that function with the next, and so on.
So using your last example:
((comp #(.toUpperCase %) #(apply str %) take) 5 "hello world") ;=> "HELLO"
The equivalent only using chain (no reduce) is:
((chain identity
(chain (chain #(.toUpperCase %)
#(apply str %))
take))
5 "hello world")
;=> "HELLO"
At a fundamental level, reduce is about iteration. Here's what an implementation in an imperative style might look like (ignoring the possibility of multiple arities, as Clojure's version supports):
def reduce(f, init, seq):
result = init
for item in seq:
result = f(result, item)
return result
It's just capturing the pattern of iterating over a sequence and accumulating a result. I think reduce has a sort of mystique around it which can actually make it much harder to understand than it needs to be, but if you just break it down you'll definitely get it (and probably be surprised how often you find it useful).