I was having a discussion with someone online, and I was trying to explain that let
is very much a functional construct, though they were under the impression since it allowed you to do imperative programming in Clojure it was thus imperative.
So finally, I came up with the following explanation, and in the process I feel I have quite simply explained the reason why you’ll hear that in functional programming the order of execution doesn’t matter:
See how let
:
(let [a 1
a (+ (inc a) (inc a))
_ (println a)
a (+ a a)]
a)
Can be rewritten simply as a nested composition of anonymous functions:
((fn[a]
((fn[a]
((fn[_]
((fn[a]
a)))
(println a))
(+ a a))
(+ (inc a) (inc a))))
1)
The latter representation you would consider functional programming no? Well the let is just syntactic sugar for it and can be rewritten in terms of only composed anonymous functions (aka lambdas).
And being functional, you can reduce it with variable substitution, which gets rid completely of all the variables, at compile time, and the evaluation will give the same result:
(do
(println (+ (inc 1) (inc 1)))
(+ (+ (inc 1) (inc 1))
(+ (inc 1) (inc 1))))
From the let form, or from its corresponding anonymous function form:
((fn[_]
((fn[]
(+ (+ (inc 1) (inc 1))
(+ (inc 1) (inc 1))))))
(println (+ (inc 1) (inc 1))))
In this case you see more clearly that the side-effect causes impurity, since it can’t really be reduced, it’s only in this case that order dependence matters, and so we can’t eliminate the wrapping function, and this relies purely on Clojure’s left to right argument evaluation ordering which allows you to mix/match side-effects within pure functions with predictable effect timing.
So here what happens you reduce that function into a do-block (which is Clojure’s imperative form):
(do
(println (+ (inc 1) (inc 1)))
((fn[]
(+ (+ (inc 1) (inc 1))
(+ (inc 1) (inc 1))))))
And now you can further reduce the pure parts, which takes us back to what we had when we reduced the let:
(do
(println (+ (inc 1) (inc 1)))
(+ (+ (inc 1) (inc 1))
(+ (inc 1) (inc 1))))
And finally this can be reduced further:
(do
(println (+ 2 2))
(+ (+ 2 2)
(+ 2 2)))
(do
(println 4)
(+ 4
4))
To our most reducible form:
(do
(println 4)
8)
This reduction can all happen in parallel or in any order, and the result will always be the same.
Ok, and this last bit is very important, this is what people mean when they say that in functional programming the order of execution doesn’t matter. The side-effects must still be sequenced in their correct order, but all the computation can happen in arbitrary order, because the computation doesn’t rely on a sequence of instructions that mutates shared memory locations like it does in the imperative programming paradigm, instead it relies on this “reduction” process I described which as you see you are free to reduce each part in whatever order you want (even in parallel), you’ll always end up with the same thing in the end.