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.
Lists end with Nil
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
Listclass. 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 aretakeandtakeWhile.
Understanding how the sum example ran
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.
How the recursion works (“going down”)
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
sumis called, thematchexpression sees that the givenListdoesn’t match theNilelement, so control flows to the secondcasestatement. - The second
casestatement matches theListpattern, then splits the incoming list of1::2::3::4::Nilinto (a) a head element of1and (b) the remainder of the list,2::3::4::Nil. The remainder — the tail — is then passed into anothersumfunction call. - A new instance of
sumreceives the list2::3::4::Nil. It sees that this list does not match theNilelement, so control flows to the secondcasestatement. - That statement matches the
Listpattern, then splits the list into a head element of2and a tail of3::4::Nil. The tail is passed as an input parameter to anothersumcall. - A new instance of
sumreceives the list3::4::Nil. This list does not match theNilelement, so control passes to the secondcasestatement. - The list matches the pattern of the second
casestatement, which splits the list into a head element of3and a tail of4::Nil. The tail is passed as an input parameter to anothersumcall. - A new instance of
sumreceives the list4::Nil, sees that it does not matchNil, and passes control to the secondcasestatement. - The list matches the pattern of the second
casestatement. The list is split into a head element of4a tail ofNil. The tail is passed to anothersumfunction call. - The new instance of
sumreceivesNilas an input parameter, and sees that it does match theNilpattern in the firstcaseexpression. At this point the firstcaseexpression is evaluated. - The first
caseexpression 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.
How the unwinding works (“coming back up”)
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
suminstance — the one that receivedList()— returns0. This happens becauseList()matchesNilin the firstcaseexpression. - This returns control to the previous
suminstance. The secondcaseexpression of thatsumfunction hasreturn 4 + sum(Nil)as its return value. This is reduced toreturn 4 + 0, so this instance returns4. (I didn’t use areturnstatement in the code, but it’s easier to read this now if I say “return.”) - Again, this returns control to the previous
suminstance. Thatsuminstance hasreturn 3 + sum(List(4))as the result of its secondcaseexpression. You just saw thatsum(List(4))returns4, so thiscaseexpression evaluates toreturn 3 + 4, or7. - Control is returned to the previous
suminstance. Its secondcaseexpression 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
sumfunction call. Its secondcaseexpression 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 firstsuminstance.
Initial visuals of how the recursion works
One way to visualize how the recursive sum function calls work — the “going down” part — is like this:

After that, when the end condition is reached, the “coming back up” part — what I call the unwinding process — looks like this:

If this isn’t clear, fear not, in the next lesson I’ll show a few more visual examples of how this works.