|
| 1 | +# About |
| 2 | + |
| 3 | +A function is [tail-recursive][recursion-tc] if the _last_ thing executed by the function is a call to itself. |
| 4 | + |
| 5 | +Each time any function is called, a _stack frame_ with its local variables and arguments is put on top of [the function call stack][call-stack]. |
| 6 | +When a function returns, the stack frame is removed from the stack. |
| 7 | + |
| 8 | +Tail-recursive functions allow for [tail call optimization][tail-call-optimization] (or tail call elimination). |
| 9 | +It's an optimization that allows reusing the last stack frame by the next function call when the previous function is guaranteed not to need it anymore. |
| 10 | +This mitigates concerns of overflowing the function call stack, which is a situation when there are so many frames on the function call stack that there isn't any memory left to create another one. |
| 11 | + |
| 12 | +## Tail Call Optimization in Elm |
| 13 | + |
| 14 | +Under some condition, the Elm compiler is able to automatically performs an a tail call optimization when compiling to JavaScript. |
| 15 | + |
| 16 | +The optimization can happen for a recursive function when the _last_ operation in a branch is done by calling the function itself in a simple function application. |
| 17 | +Let's look at some examples: |
| 18 | + |
| 19 | +```elm |
| 20 | +factorial : Int -> Int |
| 21 | +factorial n = |
| 22 | + if n <= 1 then |
| 23 | + n |
| 24 | + else |
| 25 | + n * factorial (n-1) |
| 26 | +``` |
| 27 | + |
| 28 | +The implementation above is not tail recursive, because the last operation in the `else` branch is a multiplication `n *`. |
| 29 | + |
| 30 | +```elm |
| 31 | +factorial : Int -> Int |
| 32 | +factorial n = |
| 33 | + factorialHelper n n |
| 34 | + |
| 35 | +factorialHelper : Int -> Int -> Int |
| 36 | +factorialHelper n resultSoFar = |
| 37 | + if n <= 1 then |
| 38 | + resultSoFar |
| 39 | + else |
| 40 | + factorialHelper (n-1) (n * resultSoFar) |
| 41 | +``` |
| 42 | + |
| 43 | +The implementation above is tail recursive and will be optimized, because the last operation in the `else` branch is `factorialHelper` calling itself. |
| 44 | +This would not be possible for a function with the type signature `Int -> Int`, and in practice tail call optimization is often achieved by defining helper functions. |
| 45 | + |
| 46 | +## Edge cases |
| 47 | + |
| 48 | +In some cases, the compiler is not able to optimize recursive functions, let's look at some examples. |
| 49 | + |
| 50 | +While `f x = f (x - 1)` is tail call recursive, `f x = (x - 1) |> f` is not, because the function application `|>` is the last operation and is considered separate from `f`. |
| 51 | +Similarly, functions ending with boolean conditions `f x = x > 0 || f (x - 1)` are not tail recursive. |
| 52 | + |
| 53 | +The function `f x = if f (x - 1) then 1 else 0` is not tail recursive, because the expression in the `if` is not considered to be a branch. |
| 54 | +Similarly, a recursive call in a `case` statement would not be optimized either. |
| 55 | + |
| 56 | +The function `f x = let y = f (x - 1) in y`, is not tail recursive because it calls itself in a `let` statement, which is not considered to be a branch. |
| 57 | +However, in `f x = let g x = g (x - 1) in g x` the function `g` it tail recursive and will be optimized even though it's defined in a `let` statement. |
| 58 | + |
| 59 | +The function `f x = if x > 0 then f (x + 1) else 1 + f x` will be _partially_ optimized: the `then` branch can be optimized but the `else` branch cannot. |
| 60 | +Partial optimization is the best that can be achieved in some situations, such as traversing tree-like structures or defining co-recursive functions. |
| 61 | + |
| 62 | +[recursion-tc]: https://en.wikipedia.org/wiki/Tail_call |
| 63 | +[call-stack]: https://en.wikipedia.org/wiki/Call_stack |
| 64 | +[tail-call-optimization]: https://jfmengels.net/tail-call-optimization/ |
0 commit comments