Turing developed the Turing Machine and Church developed the lambda calculus. The Turing Machine model was an imperative model that emphasized computing using state changes via updates to squares on a tape. The lambda calculus was a functional model that emphasized computing by composing functions and using parameters to communicate state information between functions. It derives its name from the greek character λ, which Church used to introduce parameters to his functions. It was eventually proven that these two models, as well as several other models of computing that were developed at about the same time, were equivalent in terms of their expressiveness, or, put another way, in terms of the set of algorithms they could express. This led Church to conjecture that all intuitively appealing models of computing would be equally powerful as well. This conjecture became known as Church's thesis.
Strictly speaking, most imperative languages like C, C++, and Java, treat functions as 2nd class values because you cannot dynamically create a new function and assign it to a variable. By contrast, many functional languages allow functions to create and return functions. For example, the user of a spreadsheet creates a function each time the user types a formula into a cell. A functional language can handle this by passing the code input by a user to a function constructor and then returning the resulting function.
(list (list 3 6 3) (list 3) (list 4 5 (list 1 2 3)))While one can often do a single level of initialization in imperative languages, it is frequently not possible to do nested initialization, as in the above example.
x := 0; i := 1; j := 100; while i < j do x := x + i*j; i := i + 1; j := j - 1 end while return xWith recursion the code becomes f(0,1,100), where
f(x,i,j) { if i < j then f(x+i*j, i+1, j-1) else x }Notice that the recursive code contains no side effects, because each "update" to x, i, and j occurs with fresh x, i, and j variables in a new stack frame. However, it still manages to modify the state by performing the necessary state changes in the argument expressions. This sort of state modification is often called a continuation, since the computation is continued in another function invocation.
f(0) = 1 f(1) = 1 f(n) = n * f(n-1)and this recurrence can be nicely modeled using the following recursive function:
factorial(n) { if (n == 0 or n == 1) return 1 else return n * f(n-1) }Similarly the greatest common divisor problem can be elegantly written using recursion:
int gcd(int a, int b) { if (a == b) return a; else if (a > b) return gcd(a - b, b); else return gcd(a, b - a);Some other natural recursive problems involve:
The compiler optimizes a tail recursive function by re-using the stack frame for the function, rather than creating a new one, and looping back to the beginning of the function after any recursive call. For example, it can rewrite the above gcd function as follows:
int gcd(int a, int b) { start: if (a == b) return a; else if (a > b) { a = a - b; goto start; } else { b = b -a; goto start; } }Note that while the code executes using iteration, the programmer's view was a recursive one. Also note why this strategy fails with the factorial function. If we try to re-use factorial's stack frame, then the value of n is corrupted when we return from each recursive call. In fact it will always be 1 on the return from the recursive call, and hence re-using the stack frame will cause the factorial function to always return 1.
factorial(n, product) { if (n == 0 or n == 1) return product else return fact(n-1, product * n) } ... print factorial(5, 1); // computes and prints 5!Personally I do not like this tail-recursive version nearly as much as the original version. I think it is much less elegant and much harder to read and comprehend. This is one reason I dislike functional programming. If the problem is not naturally tail-recursive, then to make it efficient, one often has to re-write it in an imperative style, which defeats the whole purpose of writing a functional program. Here's another example using fibonacci numbers. It has an easy recurrence relation (according to Wikipedia, the modern sequence is written the following way):
fib0 = 0 fib1 = 1 fibn = fibn-1 + fibn-2and a natural functional implementation:
fib(n) { if (n == 0) return 0 else if (n == 1) return 1 else return fib(n-1) + fib(n-2) }Unfortunately this implementation is horribly inefficient. It is in fact exponential, as you can see if you try tracing the actual sequence of function calls. In C you could write a linear time implementation as:
int fib(int n) { int f1 = 0; f2 = 1; int i; for (i = 2; i <= n; i++) { int temp = f1 + f2; f1 = f2; f2 = temp; } return f2; }Using this implementation as a template and also assuming I can use nested functions, I can write a linear time continuation-based, functional version for the fibonacci sequence:
fib(n) { fib-helper(f1, f2, i) { if (i == n) return f2; else return fib-helper(f2, f1 + f2, i+1); } return fib-helper(0, 1, 0); }While I think this example helps show the utility of nested functions, I think it also again shows the ugliness that mars functional programming when we have to worry about efficiency. The original elegance of the fibonacci program has been destroyed, and in my opinion, the continuation-based version is much harder to understand and comprehend. You may draw different conclusions based on your own background and proclivities.