Picture the following scene: you are in a job interview, and the interviewer defines a tree-based data structure and asks you to traverse it. You do a good quick job, maybe using recursion:
// provided by the interviewer: nonempty rose trees carrying the values on the internal nodes
interface ITree<x> {
value: x;
children: Array<ITree<x>>;
}
// you come up with:
function traverse<x>(tree: ITree<x>, visit: (arg: ITree<x>) => void) {
visit(tree);
for (const subtree of tree.children) {
traverse(subtree, visit);
}
}
“Great,” they say, “but this will hit the nodes of the tree in depth-first-order, so what if our tree was infinite or rapidly branching or something and you had to modify this into a breadth-first search instead?”
If you’ve seen the approach before it’s not too hard, you just keep a queue. So you start to write something like
const queue: Array<ITree<x>> = [tree];
while (queue.length > 0) {
const subtree = queue.shift();
if (visit(subtree)) {
return { type: 'found', value: subtree };
}
// otherwise
for (const child of subtree.children) {
queue.push(child);
}
}
return { type: 'not found' };
The interviewer then asks, “okay, but what I liked about the previous algorithm was that traverse
was recursive. What if I asked you to do this recursively?”
You could, of course, object programmers must optimize for their colleagues’ understanding first, and code clarity will suffer from this change. Some folks I know would definitely say, “What does that have to do with writing quality software?” and then prepare to leave the interview, questioning whether any of these interview questions can help to hire quality programmers.
We face a minor danger here though. We have a word for true statements said in place of actually accomplishing a challenge: English calls them “excuses” and even when “valid” an excuse does not actually rise to the occasion. Maybe subconsciously you think that working out the recursive version could take all afternoon. It won’t. More importantly I found this exercise very important when I started to learn another programming language, Haskell.
The designers of Haskell built a functional programming language: “everything is a function.” In it,
we do not have for(_;_;_)
or while(_)
loops. The story behind this is a little involved. We can
most naturally view most languages in an imperative model of computation: like how let x = 1
means
“create a new (mutable) name in the current block called x
, and then point that name at the value
1
” in JavaScript. We could say that the language consists of commands.
Functional programming comes from a simpler model of computation, the principle of substitution. You write a template which takes a named parameter and contains a “body;” when this template gets “applied” to a value, then the computer substitutes every use of that name in that body with that value. And the principle of substitution, while it plays very nice with definitions, does not usually play nice with mutable things. FP says, “you mean after I have performed this substitution I need to still keep track of what I substituted, so that when you change this value I can change all 5 instances of it in my filled-in template? Except that you want to change the value while I am still simplifying and reducing parts of this template?” You rapidly find yourself reinventing that imperative model just from variables alone. I am not quite sure why this is. Template substitution has a weaker, more implicit, understanding of time than the imperative model; perhaps variables are just naturally easier to phrase with a stronger, more explicit understanding of time.
But if you don't have true variables then what are you going to do with while(_)
? Whatever gets
substituted into its parentheses must remain unchanged, leaving while(true)
and while(false)
.
“Aha,” you say: “I will just use break
statements!” ... but how are you going to phrase those as
something that plays nice with template substitution? I am not saying it is impossible, just that
Haskell wisely waved a white flag and surrendered.
So everyone who comes to Haskell now needs to learn how to do looping without for
and while
. We
instead use recursive functions—functions that are defined in terms of themselves. It turns out that
these can be rewritten with the same jump instructions that for
and while
use by the compiler so
long as every recursive call looks like,
// within a definition of f(x, y, z)
return f(newX, newY, newZ);
Note that there cannot be manipulation of the value afterwards, like return f(...) + 1
is not
allowed: that semicolon is exactly where it needs to be. In some ways this makes things worse! Not
only do you have to write your loop as a recursion, you must write it in this “tail-recursive”
style!
So I wanted to show you a really simple approach in JS, before you even get to Haskell and its brethren, that I call “iterorecursive” algorithms. If you are ever in a job interview and asked to reverse a linked list recursively, and you know the iterorecursive trick, you can easily ace the above question. Once you have an iterative algorithm, you should not have problems converting it to a recursive one, if you know this trick. And because it came from the iterative algorithm, your solution will naturally be tail-recursive.
- Write out the loop, maybe in pseudocode. (If this were a job interview you might be able to say “would you mind if we thought it through iteratively first?” if you have, say, been asked to recursively reverse a linked list.)
- Identify what variables you are modifying in each step of the loop.
- Define a function which will run one step of the loop. Each of the above variables enters as an
argument to that function.
- This could be either the actual function that you're going to return, or an internal “work” function which does the loop.
- Especially if you need to preprocess something coming into the function, you may need to have this internal “work” function. Also if you seem to write the same postprocessing logic in two or three places, I would find it worth considering.
- If you don’t, then you can use the JS syntax
f(x, y, var1=startingValue, var2=startingValue2)
to fill in the starting values for the variables, and then you can just return the one function. - A good name for internal work functions is
go
, if you don't have a more descriptive name.
- Write the loop-failure condition at the top of this function, the one that will exit the loop,
as an
if
statement. Figure out what value you want to produce when the loop is complete, and return it inside of thatif
statement.- What really helps is a trick I picked up in my physics degree: write down the simplest input, and what the answer is for the simplest example, then fill in the combination of arguments which makes that simplest example correct.
- Maybe write some
const
expressions, if you want to destructure an object or you are going to need to use a sub-expression twice and you would rather name it here. - Start with
return myFunctionName(...)
then fill in the...
with all of the new values of arguments that the single step of the loop would compute. - Surprisingly, you are probably done and it probably works. Test it out a bit!