Recursion
Recursion is sometimes more confusing because it's actually more declarative. You don't see exactly how everything works.
Recursion usually works by breaking down the problem into smaller pieces and adding together the small solutions. As long as you can understand the solution to the simple problem and how those solutions can be combined together, then that's enough.
Base Condition Location
It's most common to make your base condition return nothing. However, the problem with this is that the last function call in the stack feels like a waste.
For example, in a countVowels
recursion, you don't need to return an empty string count.
A performance optimization would be to have your base condition do more of a lookahead to check if the function call is worth making again.
Stack Frames & Memory Limits
A serious limitation with recursion being used in production applications is that it creates too many stack frames, making it too memory-intensive.
Tail call optimization
To address the problem of stack frames taking up too much memory, some compilers employ tail call optimization.
The basic idea is this: if the last operation in the function is the same recursive function (a tail call), then we know there is nothing left to do in the current function. Therefore, we can drop it and maintain O(1)
constant memory space.
Proper tail calls
The good news is that ES6 now supports proper tail calls. There are a few criteria that must be met for a tail call to be proper:
Strict mode must be on:
"use strict"
The tail call must happen in the
return
statement, and it should return the return value of the tail call
Refactoring into proper tail call form
Recall the countVowels
function. It is not in proper tail call form because it has a +
operation after the function call.
The key consideration is that currently the count of the vowels is being stored across the stack frames. Where else can you keep track of the count?
You can track it in an argument of the function (i.e. passing it onto the next stack frame).
The trouble with this solution though is that it's a bit of a leaky abstraction. Why do we have to pass 0
as the initial value? Shouldn't the function be taking care of that part?
To hide 0
, we can curry it as the first specified argument.
Alternatives to Tail Calls
What do we do if our compiler doesn't support tail call optimization?
There are at least 2 techniques worth learning to improve recursion:
Continuation-passing style
Trampolines
Continuation-passing style
Continuation-passing style is the style of passing a callback. By doing this, you can somehow defer loading the stack. Instead, the memory usage goes to the heap.
Here's how continuation-passing style looks:
Roughly, what the code is doing is deferring the work of computing the count until you hit the base case. At that point, all the computation happens at once.
Trampolines
Trampolines are adapter functions to help you achieve O(1)
memory space. The basic idea is this: instead of making the recursive function call, trampolilnes return a function that will make the next call.
In rough pseudocode:
Call the function.
While the function continues returning a function, call those functions.
Once the return value is not a function, return the return value.
When refactoring your recursive function, the only thing you have to do is wrap your recursive call in a function:
Note: The reason the returned function can retain memory of the information we need (count
and str
) is because of closure. It doesn't need ever-increasing stack frames.
Important: One huge benefit of trampolines is that it is very easy to refactor for proper tail calls (whenever that becomes fully supported).
Last updated