|
1 | 1 | package scalatutorial.sections |
2 | 2 |
|
| 3 | +import scala.annotation.tailrec |
| 4 | + |
3 | 5 | /** @param name tail_recursion */ |
4 | 6 | object TailRecursion extends ScalaTutorialSection { |
5 | 7 |
|
| 8 | + /** |
| 9 | + * = Recursive Function Application = |
| 10 | + * |
| 11 | + * Let’s compare the evaluation steps of the application of two recursive |
| 12 | + * methods. |
| 13 | + * |
| 14 | + * First, consider `gcd`, a method that computes the greatest common divisor of |
| 15 | + * two numbers. |
| 16 | + * |
| 17 | + * Here's an implementation of `gcd` using Euclid's algorithm. |
| 18 | + * |
| 19 | + * {{{ |
| 20 | + * def gcd(a: Int, b: Int): Int = |
| 21 | + * if (b == 0) a else gcd(b, a % b) |
| 22 | + * }}} |
| 23 | + * |
| 24 | + * `gcd(14, 21)` is evaluated as follows: |
| 25 | + * |
| 26 | + * {{{ |
| 27 | + * gcd(14, 21) |
| 28 | + * if (21 == 0) 14 else gcd(21, 14 % 21) |
| 29 | + * if (false) 14 else gcd(21, 14 % 21) |
| 30 | + * gcd(21, 14 % 21) |
| 31 | + * gcd(21, 14) |
| 32 | + * if (14 == 0) 21 else gcd(14, 21 % 14) |
| 33 | + * gcd(14, 7) |
| 34 | + * gcd(7, 14 % 7) |
| 35 | + * gcd(7, 0) |
| 36 | + * if (0 == 0) 7 else gcd(0, 7 % 0) |
| 37 | + * 7 |
| 38 | + * }}} |
| 39 | + * |
| 40 | + * Now, consider `factorial`: |
| 41 | + * |
| 42 | + * {{{ |
| 43 | + * def factorial(n: Int): Int = |
| 44 | + * if (n == 0) 1 else n * factorial(n - 1) |
| 45 | + * }}} |
| 46 | + * |
| 47 | + * `factorial(4)` is evaluated as follows: |
| 48 | + * |
| 49 | + * {{{ |
| 50 | + * factorial(4) |
| 51 | + * if (4 == 0) 1 else 4 * factorial(4 - 1) |
| 52 | + * 4 * factorial(3) |
| 53 | + * 4 * (3 * factorial(2)) |
| 54 | + * 4 * (3 * (2 * factorial(1))) |
| 55 | + * 4 * (3 * (2 * (1 * factorial(0))) |
| 56 | + * 4 * (3 * (2 * (1 * 1))) |
| 57 | + * 24 |
| 58 | + * }}} |
| 59 | + * |
| 60 | + * What are the differences between the two sequences? |
| 61 | + * |
| 62 | + * One important difference is that in the case of `gcd`, we see that |
| 63 | + * the reduction sequence essentially oscillates. It goes from one call to |
| 64 | + * `gcd` to the next one, and eventually it terminates. In between we have |
| 65 | + * expressions that are different from a simple call like if then else's |
| 66 | + * but we always come back to this shape of the call of `gcd`. If we look at |
| 67 | + * `factorial`, on the other hand we see that in each couple of steps we add |
| 68 | + * one more element to our expressions. Our expressions becomes bigger and |
| 69 | + * bigger until we end when we finally reduce it to the final value. |
| 70 | + * |
| 71 | + * = Tail Recursion = |
| 72 | + * |
| 73 | + * That difference in the rewriting rules actually translates directly to a |
| 74 | + * difference in the actual execution on a computer. In fact, it turns out |
| 75 | + * that if you have a recursive function that calls itself as its last action, |
| 76 | + * then you can reuse the stack frame of that function. This is called ''tail |
| 77 | + * recursion''. |
| 78 | + * |
| 79 | + * And by applying that trick, a tail recursive function can execute in |
| 80 | + * constant stack space, so it's really just another formulation of an |
| 81 | + * iterative process. We could say a tail recursive function is the functional |
| 82 | + * form of a loop, and it executes just as efficiently as a loop. |
| 83 | + * |
| 84 | + * If we look back at `gcd`, we see that in the else part, `gcd` calls itself |
| 85 | + * as its last action. And that translates to a rewriting sequence that was |
| 86 | + * essentially constant in size, and that will, in the actual execution on a |
| 87 | + * computer, translate into a tail recursive call that can execute in constant |
| 88 | + * space. |
| 89 | + * |
| 90 | + * On the other hand, if you look at `factorial` again, then you'll see that |
| 91 | + * after the call to `factorial(n - 1)`, there is still work to be done, |
| 92 | + * namely, we had to multiply the result of that call with the number `n`. |
| 93 | + * So, that recursive call is not a tail recursive call, and it becomes evident in |
| 94 | + * the reduction sequence, where you see that actually there’s a buildup of |
| 95 | + * intermediate results that we all have to keep until we can compute the |
| 96 | + * final value. So, `factorial` would not be a tail recursive function. |
| 97 | + * |
| 98 | + * Both `factorial` and `gcd` only call itself but in general, of course, a |
| 99 | + * function could call other functions. So the generalization of tail |
| 100 | + * recursion is that, if the last action of a function consists of calling |
| 101 | + * another function, maybe the same, maybe some other function, the stack |
| 102 | + * frame could be reused for both functions. Such calls are called ''tail calls''. |
| 103 | + * |
| 104 | + * = Tail Recursion in Scala = |
| 105 | + * |
| 106 | + * In Scala, only directly recursive calls to the current function are optimized. |
| 107 | + * |
| 108 | + * One can require that a function is tail-recursive using a `@tailrec` annotation: |
| 109 | + * |
| 110 | + * {{{ |
| 111 | + * @tailrec |
| 112 | + * def gcd(a: Int, b: Int): Int = … |
| 113 | + * }}} |
| 114 | + * |
| 115 | + * If the annotation is given, and the implementation of `gcd` were not tail |
| 116 | + * recursive, an error would be issued. |
| 117 | + * |
| 118 | + * = Exercise = |
| 119 | + * |
| 120 | + * Complete the following definition of a tail-recursive version of `factorial`: |
| 121 | + */ |
| 122 | + def tailRecFactorial(res0: Int, res1: Int, res2: Int): Unit = { |
| 123 | + def factorial(n: Int): Int = { |
| 124 | + @tailrec |
| 125 | + def iter(x: Int, result: Int): Int = |
| 126 | + if (x == res0) result |
| 127 | + else iter(x - res1, result * x) |
| 128 | + |
| 129 | + iter(n, res2) |
| 130 | + } |
| 131 | + |
| 132 | + factorial(3) shouldBe 6 |
| 133 | + factorial(4) shouldBe 24 |
| 134 | + } |
6 | 135 | } |
0 commit comments