Instead of using the non-functional for loop (loop control
variable), we can process lists recursively: if the list is empty, then we're
done, otherwise we do something with the head of the list, and then recursively
process the tail:
// fetch member at a given position
def get[T](pos: Int, vals: List[T]): T = {
if (vals == Nil) throw new
Exception("List is too short");
if (pos < 0) throw new
Exception("position must not be negative");
if (pos == 0) vals.head else get(pos - 1, vals.tail)
}
// search for an element
def member[T](elem: T, vals: List[T]): Boolean = {
if (vals == Nil) false
else if (elem == vals.head) true
else member(elem, vals.tail)
}
// add an element at a given position
def add[T](elem: T, pos: Int, vals: List[T]): List[T] = {
if (vals == Nil && pos > 0)
throw new Exception("List is too short");
if (pos < 0) throw new
Exception("position must not be negative");
if (pos == 0) elem:: vals
else vals.head::add(elem, pos - 1, vals.tail)
}
// remove element t a given position
def rem[T](pos: Int, vals: List[T]): List[T] = {
if (vals == Nil) throw new
Exception("List is too short");
if (pos < 0) throw new
Exception("position must not be negative");
if (pos == 0) vals.tail
else vals.head::rem(pos - 1, vals.tail)
}
Notes:
·
These are all good examples of generic functions
·
All of these functions are non-destructive. For
example:
scala> val ages = List(21, 13, 12, 44, 18, 19)
ages: List[Int] = List(21, 13, 12, 44, 18, 19)
scala> rem(2, ages)
res8: List[Int] = List(21, 13, 44, 18, 19) // 12 missing in result
scala> ages
res9: List[Int] = List(21, 13, 12, 44, 18, 19) // 12 still there in ages
·
How would you implement these functions using
iteration (for loops)
Processing a length n list usually requires O(n) time for both recursion and iteration. However,
iteration only requires O(1) space, while recursion
makes n recursive calls that gobble up O(n) space. We can remedy this situation
in languages that are optimized to use tail recursion. In a tail recursive
function no additional work is pending after the recursive call. The member
function above is tail recursive, while the rem function is not. (Why?)
Each of the following three functions correctly returns the
sum of a list of numbers in O(n) time, where n = the
length of the list:
def iterSum(nums: List[Double]): Double = {
var result = 0.0
for(i <- nums) result += i
result
}
def recursiveSum(nums: List[Double]): Double = {
if (nums == Nil) 0 else nums.head +
recursiveSum(nums.tail)
}
def tailRecursiveSum(nums: List[Double]): Double = {
def helper(result: Double, unseen:
List[Double]): Double = {
if (unseen == Nil) result else
helper(result + unseen.head, unseen.tail)
}
helper(0.0, nums)
}
However, iterSum and tailRecursiveSum consume O(1)
memory, while recursiveSum consumes O(n) memory. To
see this, trace the computations of all three given a list of three numbers.
Notes
·
Strictly speaking, tailRecursiveSum
isn't recursive at all. But its internal helper function is tail recursive.
·
Adding an internal helper function that passes a
partial result along with the unseen tail of a list to its recursive call is a
typical way of making tail recursive functions.
·
Every recursive function can be turned into a
tail recursive function, making non-functional iterations unnecessary.
·
How would you reimplement
add and rem as tail recursions? (Hard.)