3 ms·
So, it's not like loops are an inherently imperative idea, so much as that they typically are something that only make obvious sense in an imperative world. Le
by angersock 12y ago
So, it's not like loops are an inherently imperative idea, so much as that they typically are something that only make obvious sense in an imperative world.
Let's start with a simple function that counts down from five in Javascript:
var countDownFromFive = function() {
console.log(5);
console.log(4);
console.log(3);
console.log(2);
console.log(1);
console.log(0);
}
So, that's the purely imperative way of doing things. We notice a pattern, though: the numbers are just a list from [5,4,3,2,1,0]. So, we could just describe them as the series S(n) = n + S(n-1), where n >= 0. So, we write it in that form:
var countDownFrom = function(num) {
console.log( num );
if (num >= 0) {
countDownFrom(num - 1);
}
}
And then our count down from five becomes:
var countDownFromFive = function() {
countDownFrom(5);
}
So, that's great, and that's a nice recursive definition of the problem. The problem is, though, that for a really big number, say 2^32, we've got to store a stack of results, and that blows everything up. We can fix this with tail call optimization, because we know we can reuse the same stack frames, but that's an optimization many low-level languages don't have.
Indeed, until we've invented them, we don't even have the necessary concepts of a stack with function frames. So, let's look at our problem again.
Let's pretend we're in a very limited assembly language, with just registers:
LOAD registerA 5
PRINT registerA
LOAD registerA 4
PRINT registerA
LOAD registerA 3
PRINT registerA
LOAD registerA 2
PRINT registerA
LOAD registerA 1
PRINT registerA
LOAD registerA 0
PRINT registerA
So, that mirrors our pure imperative case. Aha, but let's say we're smarter than that, and we have a branch instruction and a decrement operator:
start:
LOAD registerA 5
doPrint:
PRINT registerA
DECREMENT registerA
JUMP_IF_NOT_NEGATIVE registerA doPrint
finish:
HALT
So, even without the notion of a function stack, we can still print a list of numbers. We are now experts at counting down from five.
~
The thing we see, though, is that the difference in implementation comes from basically whether or not you can compose functions: if you can, you use the functional approach. If you can't, you refactor the problem to have an explicit accumulator (your loop variable).
Loops can be considered an inherently imperative idea because you have this changing context wrapping the function being invoked (a mutating accumulator, if you will)--in the functional world, you merely wrap another function which itself creates a new context with the desired mutation, and that function in turn calls another function setting up its own context and so on and so forth. It's somewhat handier to prove correctness and behavior of these recursive functions compared with their mutating accumulator imperative solutions; at the same time, they are slower without compiler support.
- rifung 12y agoThat last paragraph really helped make this clear. Thanks! I guess I never thought about it that way but the logic in the loops could be considered their own functions, except they actually can reference and alter variables outside the loop which is kind of against the ideas of functional programming. Awesome! Thanks again
- angersock 12y agoGlad I could help! One last thing: the core idea of functional programming is mapping state to state, the same way a transform matrix maps a vector to another vector. So, to accomplish something, you string together functions to map you from the state you have to an ultimate state that you want. For something like calculating the average of a set of numbers, this would be: [ set of numbers ] -> function maps from the "set of numbers space" to the "sum of all numbers in a set space" --> function that maps from the "number space" to the "number space scaled by a value (here being the number of original numbers in the set)" --> your final mean