During the past couple of months I’ve been doing 4clojure exercises regularly, and today I solved an interesting one. It’s about higher-order functions, and since I don’t have much experience with those, I found the problem a bit of a challenge. In this post I’ll share my thoughts about the exercise and couple of solutions I devised to solve the task.
4Clojure exercise #58: Function Composition
“Write a function which allows you to create function compositions. The parameter list should take a variable number of functions, and create a function that applies them from right-to-left.”
Special restrictions: comp
1
2
3
4
(= [3 2 1] ((__ rest reverse) [1 2 3 4]))
(= 5 ((__ (partial + 3) second) [1 2 3 4]))
(= true ((__ zero? #(mod % 8) +) 3 5 7 9))
(= "HELLO" ((__ #(.toUpperCase %) #(apply str %) take) 5 "hello world"))
So, whenever the exercise forbids use of certain function(s), that basically means I have to write my version of that function. By looking at the tests I have quickly come up with:
1
2
3
(fn my-comp [f g]
(fn [x]
(f (g x))))
I knew right away that it would only pass the first two tests, since it accepts fixed number of functions (2), and only one argument, but I had to start somewhere. The next thing was how to implement my-comp function to accept variable number of arguments. Clojure has an easy way for this, by using &
in front of args
(or however we want to call our collection of arguments). But, that means I cannot implement my function the same way as above, since I don’t know how many arguments I’ll have. The first method I reach for in those situations is loop/recur
. After a while I hacked this solution that works:
1
2
3
4
5
6
7
(fn my-comp2 [& fns]
(fn [& xs]
(loop [xs (apply (last fns) xs)
fns (rest (reverse fns))]
(if (seq fns)
(recur ((first fns) xs) (rest fns))
xs))))
It accepts multiple function arguments, and the inner function accepts variable number of arguments. It also passes all four tests, so I might consider I completed the task. Although this solution works, there are several problems with it. Let’s call this function with 1st test case ((my-comp2 rest reverse) [1 2 3 4])
and analyze this function:
- First, we define a function my-comp2 and we see that it accepts multiple arguments [& fns]
- Next, the inner function accepts multiple arguments [& xs]
- Then we encounter a
loop
where the following values are bound:- xs (apply (last fns) xs) - I’m applying the last argument from fns, which is
reverse
to my xs collection, which is [1 2 3 4] so the value of xs is [4 3 2 1] - fns (rest (reverse fns)) - Since functions have to be applied right-to-left, I needed to reverse my sequence of functions, and to leave out the function I already applied to arguments when I declared xs binding.
- xs (apply (last fns) xs) - I’m applying the last argument from fns, which is
- Next is
(if (seq fns)...
which I first wrote as(if fns (then branch) (else branch))
but I got weridExecution error (NullPointerException)
(i found out thatif
call causes this error by using CIDER Debugger). I wasn’t sure why I got this error, sincefns
should be a seq AFAIK. I managed to “solve” it by(seq fns)
. It happens whenrest
is applied tofns
containing one argument, and it returns empty seq, which evaluates to true, and then ((first fns) xs) produces this error. Callingseq
on empty sequence returns nil, and we get expected results. - As long as we have functions in our seq this code executes:
(recur ((first fns) xs) (rest fns))
- New value ofxs
is the result of first function in line executed on currentxs
value, and new value offns
is everything except the function that was just executed onxs
- When there are no functions left to execute (our
fns
is empty), we just return ourxs
collection, which is what we need to pass the exercise.
This solution works, but there are several problems with it. Mainly, when the xs
and fns
are declared. For xs
the (apply (last fns) is used, which has to go through the whole seq of functions in order to get to last and execute it. Then, when fns
is declared the reverse
function is applied, which is not lazy, so it rearranges our seq, and proceeds to apply the rest
function on result. Although this way I was able to successfully complete the exercise I knew there must be a better way to implement this function. So I thought maybe it wasn’t a bad idea to write a function that will behave differently depending on the number of functions it got. After some time I came up with:
1
2
3
4
5
6
7
8
9
(fn my-comp3
([f]
(fn [& xs]
(apply f xs)))
([f g]
(fn [& xs]
(f (apply g xs))))
([f g & fns]
(reduce my-comp3 (my-comp3 f g) fns)))
Let’s analyze. We have three function bodies. First, which gets only one function and applies it to any number of arguments this function gets. Second, and pretty similar to my-comp
at the beginning of this article, gets two functions, applies second function to arguments it gets, then calls first function on the result of applying second function to xs. Third body is most interesting, as it is recursive call to the function itself. It accepts first function as f
, second as g
and remaining functions in fns
seq. The body itself (reduce my-comp3 (my-comp3 f g) fns)
is the most amazing thing. Since fn [& xs]
is called inside of fn [f g]
, reduce first needs to get to the last function, and then it will get applied to xs, so imagine our fns
consists of (fn1 fn2 fn3 fn4 fn5), then reduce will produce a call that looks like (fn1 (fn2 (fn3 (fn4 (apply fn5 xs)))))
. This looks like much cleaner & more efficient solution.
I hope you had a nice time reading my experience with this. Feel free to comment and to leave any suggestion on how would you approach solving this problem.