Recursion: How Recursive Function Calls Work
An important point to understand about recursive function calls is that just as they “wind up” as they are called repeatedly, they “unwind” rapidly when the function’s end condition is reached.
In the case of the sum
function, the end condition is reached when the Nil
element in a List
is reached. When sum
gets to the Nil
element, this pattern of the match
expression is matched:
case Nil => 0
Because this line simply returns 0
, there are no more recursive calls to sum
. This is a typical way of ending the recursion when operating on all elements of a List
in recursive algorithms.
As I wrote in the earlier List
lesson, a literal way to create a List
is like this:
1 :: 2 :: 3 :: 4 :: Nil
This is a reminder that with any Scala List
you are guaranteed that the last List
element is Nil
. Therefore, if your algorithm is going to operate on the entire list, you should use:
case Nil => ???
as your function’s end condition.
This is the first clue about how the unfolding process works.
Note 1: This is a feature of the Scala
List
class. You’ll have to change the approach if you work with other sequential collection classes likeVector
,ArrayBuffer
, etc. (More on this later in the book.)Note 2: Examples of functions that work on every element in a list are
map
,filter
,foreach
,sum
,product
, and many more. Examples of functions that don’t operate on every list element aretake
andtakeWhile
.
A good way to understand how the sum
function example ran is to add println
statements inside the case
expressions.
First, change the sum
function to look like this:
def sum(list: List[Int]): Int = list match {
case Nil => {
println("case1: Nil was matched")
0
}
case head :: tail => {
println(s"case2: head = $head, tail = $tail")
head + sum(tail)
}
}
Now when you run it again with a List(1,2,3,4)
as its input parameter, you’ll see this output:
case2: head = 1, tail = List(2, 3, 4)
case2: head = 2, tail = List(3, 4)
case2: head = 3, tail = List(4)
case2: head = 4, tail = List()
case1: Nil was matched
That output shows that sum
is called repeatedly until the list is reduced to List()
(which is the same as Nil
). When List()
is passed to sum
, the first case
is matched and the recursive calls to sum
come to an end. (I’ll demonstrate this visually in the next lesson.)
The book, Land of Lisp states, “recursive functions are list eaters,” and this output shows why that statement is true.
Keeping in mind that List(1,2,3,4)
is the same as 1::2::3::4::Nil
, you can read the output like this:
The first time
sum
is called, thematch
expression sees that the givenList
doesn’t match theNil
element, so control flows to the secondcase
statement.The second
case
statement matches theList
pattern, then splits the incoming list of1::2::3::4::Nil
into (a) a head element of1
and- the remainder of the list,
2::3::4::Nil
. The remainder — the tail — is then passed into anothersum
function call.
- the remainder of the list,
A new instance of
sum
receives the list2::3::4::Nil
. It sees that this list does not match theNil
element, so control flows to the secondcase
statement.That statement matches the
List
pattern, then splits the list into a head element of2
and a tail of3::4::Nil
. The tail is passed as an input parameter to anothersum
call.A new instance of
sum
receives the list3::4::Nil
. This list does not match theNil
element, so control passes to the secondcase
statement.The list matches the pattern of the second
case
statement, which splits the list into a head element of3
and a tail of4::Nil
. The tail is passed as an input parameter to anothersum
call.A new instance of
sum
receives the list4::Nil
, sees that it does not matchNil
, and passes control to the secondcase
statement.The list matches the pattern of the second
case
statement. The list is split into a head element of4
a tail ofNil
. The tail is passed to anothersum
function call.The new instance of
sum
receivesNil
as an input parameter, and sees that it does match theNil
pattern in the firstcase
expression. At this point the firstcase
expression is evaluated.The first
case
expression returns the value0
. This marks the end of the recursive calls.
At this point — when the first case
expression of this sum
instance returns 0
— all of the recursive calls “unwind” until the very first sum
instance returns its answer to the code that called it.
That description gives you an idea of how the recursive sum
function calls work until they reach the end condition. Here’s a description of what happens after the end condition is reached:
The last
sum
instance — the one that receivedList()
— returns0
. This happens becauseList()
matchesNil
in the firstcase
expression.This returns control to the previous
sum
instance. The secondcase
expression of thatsum
function hasreturn 4 + sum(Nil)
as its return value. This is reduced toreturn 4 + 0
, so this instance returns4
. (I didn’t use areturn
statement in the code, but it’s easier to read this now if I say “return.”)Again, this returns control to the previous
sum
instance. Thatsum
instance hasreturn 3 + sum(List(4))
as the result of its secondcase
expression. You just saw thatsum(List(4))
returns4
, so thiscase
expression evaluates toreturn 3 + 4
, or7
.Control is returned to the previous
sum
instance. Its secondcase
expression hasreturn 2 + sum(List(3,4))
as its result. You just saw thatsum(List(3,4))
returns7
, so this expression evaluates toreturn 2 + 7
, or9
.Finally, control is returned to the original
sum
function call. Its secondcase
expression isreturn 1 + sum(List(2,3,4))
. You just saw thatsum(List(2,3,4))
returns9
, so this call is reduced toreturn 1 + 9
, or10
. This value is returned to whatever code called the firstsum
instance.
One way to visualize how the recursive sum
function calls work — the “going down” part — is shown in Figure [fig:recursionGoingDown].
After that, when the end condition is reached, the “coming back up” part — what I call the unwinding process — is shown in Figure [fig:recursionUnwinding].
If this isn’t clear, fear not, in the next lesson I’ll show a few more visual examples of how this works.