TL;DR: recursion is used to handle inductively defined data, which are ubiquitous.
Recursion is natural, when you operate on higher levels of abstraction. Functional programming is not just about coding with functions; it is about operating on higher levels of abstraction, where you naturally use functions. Using functions, it is only natural to reuse the same function (to call it again), from whatever context where it makes sense.
The world is built by repetition of similar/same building blocks. If you cut a piece of fabric in two, you have two pieces of fabric. Mathematical induction is at the heart of maths. We, humans, count (as in, 1,2,3...). Any inductively defined thing (like, {numbers from 1} are {1, and numbers from 2}) is natural to handle/analyze by a recursive function, according to same cases by which that thing is defined/constructed.
Recursion is everywhere. Any iterative loop is a recursion in disguise anyway, because when you reenter that loop, you reenter that same loop again (just with maybe different loop variables). So it's not like inventing new concepts about computing, it's more like discovering the foundations, and making it explicit.
So, recursion is natural. We just write down some laws about our problem, some equations involving the function we're defining which preserve some invariant (under the assumption that the function is coherently defined), re-specifying the problem in simplified terms, and voila! We have the solution.
An example, a function to calculate the length of list (an inductively defined recursive data type). Assume it is defined, and returns the list's length, non-surprisingly. What are the laws it must obey? What invariant is preserved under what simplification of a problem?
The most immediate is taking the list apart to its head element, and the rest - a.k.a. the list's tail (according to how a list is defined/constructed). The law is,
length (x:xs) = 1 + length xs
D'uh! But what about the empty list? It must be that
length [] = 0
So how do we write such a function?... Wait... We've written it already! (In Haskell, if you were wondering, where function application is expressed by juxtaposition, parentheses are used just for grouping, and (x:xs)
is a list with x
its first element, and xs
the rest of them).
All we need of a language to allow for such style of programming is that it has TCO (and perhaps, a bit luxuriously, TRMCO), so there's no stack blow-up, and we're set.
Another thing is purity - immutability of code variables and/or data structure (records' fields etc).
What that does, besides freeing our minds from having to track what is changing when, is it makes time explicitly apparent in our code, instead of hiding in our "changing" mutable variables/data. We can only "change" in the imperative code the value of a variable from now on - we can't very well change its value in the past, can we?
And so we end up with lists of recorded change history, with change explicitly apparent in the code: instead of x := x + 2
we write let x2 = x1 + 2
. It makes reasoning about code so much easier.
To address immutability in the context of tail recursion with TCO, consider this tail recursive re-write of the above function length
under accumulator argument paradigm:
length xs = length2 0 xs -- the invariant:
length2 a [] = a -- 1st arg plus
length2 a (x:xs) = length2 (a+1) xs -- the length of 2nd arg
Here TCO means call frame reuse, in addition to the direct jump, and so the call chain for length [1,2,3]
can be seen as actually mutating the call stack frame's entries corresponding to the function's parameters:
length [1,2,3]
length2 0 [1,2,3] -- a=0 (x:xs)=[1,2,3]
length2 1 [2,3] -- a=1 (x:xs)=[2,3]
length2 2 [3] -- a=2 (x:xs)=[3]
length2 3 [] -- a=3 (x:xs)=[]
3
In a pure language, without any value-mutating primitives, the only way to express change is to pass updated values as arguments to a function, to be processed further. If the further processing is the same as before, than naturally we have to invoke the same function for that, passing the updated values to it as arguments. And that's recursion.
And the following makes the whole history of calculating the length of an argument list explicit (and available for reuse, if need be):
length xs = last results
where
results = length3 0 xs
length3 a [] = [a]
length3 a (x:xs) = a : length3 (a+1) xs
In Haskell this is variably known as guarded recursion, or corecursion (at least I think it is).
The divide and conquer paradigm
. But I'm giving you a perception from the algorithmic point of view. I don't know why recursion is preferred over iteration to problems which can be solved by both. But in most of the divide and conquer type of problems, recursion is the only cjoice you have got . – Nithish Inpursuit Ofhappiness