I am experimenting with a more functional style in my JavaScript; therefore, I have replaced for loops with utility functions such as map and reduce. However, I have not found a functional replacement for while loops since tail call optimization is generally not available for JavaScript. (From what I understand ES6 prevents tail calls from overflowing the stack but does not optimize their performance.)
I explain what I have tried below, but the TLDR is: If I don't have tail call optimization, what is the functional way to implement while loops?
What I have tried:
Creating a "while" utility function:
function while(func, test, data) {
const newData = func(data);
if(test(newData)) {
return newData;
} else {
return while(func, test, newData);
}
}
Since tail call optimization isn't available I could rewrite this as:
function while(func, test, data) {
let newData = *copy the data somehow*
while(test(newData)) {
newData = func(newData);
}
return newData;
}
However at this point it feels like I have made my code more complicated/confusing for anyone else who uses it, since I have to lug around a custom utility function. The only practical advantage that I see is that it forces me to make the loop pure; but it seems like it would be more straightforward to just use a regular while loop and make sure that I keep everything pure.
I also tried to figure out a way to create a generator function that mimics the effects of recursion/looping and then iterate over it using a utility function like find or reduce. However, I haven't figured out an readable way to do that yet.
Finally, replacing for loops with utility functions makes it more apparent what I am trying to accomplish (e.g. do a thing to each element, check if each element passes a test, etc.). However, it seems to me that a while loop already expresses what I am trying to accomplish (e.g. iterate until we find a prime number, iterate until the answer is sufficiently optimized, etc.).
So after all this, my overall question is: If I need a while loop, I am programming in a functional style, and I don't have access to tail call optimization, then what is the best strategy.
I've been thinking about this question a lot. Recently I came across the need for a functional while loop.
It seems to me like the only thing this question really wants is a way to inline a while loop. There IS a way to do that using a closure.
Alternatively, if what you want is something that chains off an array, than you can use the reduce method.
None of this actually turns our while loop at its core into a function. But it does allow us the use of an inline loop. And I just wanted to share this with anyone who this might help.
Programming in the sense of the functional paradigm means that we are guided by types to express our algorithms.
To transform a tail recursive function into a stack-safe version we have to consider two cases:
We have to make a choice and this goes well with tagged unions. However, Javascript doesn't have such a data type so either we have to create one or fall back to
Object
encodings.Object Encoded
Function Encoded
Alternatively, we can create a real tagged union with a function encoding. Now our style is much closer to mature functional languages:
An example in JavaScript
Here's an example using JavaScript. Currently, most browsers do not support tail call optimisation and therefore the following snippet will fail
Trampolines
We can work around this limitation by changing the way we write repeat, but only slightly. Instead of returning a value directly or immediately recurring, we will return one of our two trampoline types,
Bounce
orDone
. Then we will use ourtrampoline
function to handle the loop.Currying slows things down a little bit too, but we can remedy that using an auxiliary function for the recursion. This is nice too because it hides the trampoline implementation detail and does not expect the caller to bounce the return value. This runs about twice as fast as the above
repeat
Clojure-style
loop
/recur
Trampolines are nice and all but it's kind of annoying to have to have to worry about calling
trampoline
on your function's return value. We saw the alternative was to use an auxiliary helper, but c'mon that's kind of annoying, too. I'm sure some of you aren't too keen about theBounce
andDone
wrappers, too.Clojure creates a specialised trampoline interface that uses a pair of functions,
loop
andrecur
– this tandem pair lends itself to a remarkably elegant expression of a programOh and it's really fast, too
The continuation monad
This is one of my favourite topics tho, so we're gonna see what this looks like with the Continuation monad. We'll also go one step further and hide the trampoline implementation detail inside of our
repeat
function using an auxiliary function (aux
) so that the caller doesn't have to worry about bouncing the return value each timeY
combinatorThe Y combinator is my spirit combinator; this answer would be incomplete without giving it some place amongst the other techniques. Most implementations of the Y combinator however, are not stack-safe and will overflow if the user-supplied function recurs too many times. Since this answer is all about preserving stack-safe behaviour, of course we'll implement
Y
in a safe way – again, relying on our trusty trampoline.Y
demonstrates the ability to extend easy-to-use, stack-safe, synchronous infinite recursion without cluttering up your function.Practicality with
while
loopBut let's be honest: that's a lot of ceremony when we're overlooking one of the more obvious potential solutions: use a
for
orwhile
loop, but hide it behind a functional interfaceFor all intents and purposes, this
repeat
function works identically to the ones provided above – except this one is about one or two gadzillion times faster (with exception to theloop
/recur
solution). Heck, it's arguably a lot easier to read too.Granted, this function is perhaps a contrived example – not all recursive functions can be converted to a
for
orwhile
loop so easily, but in such a scenario where it's possible, it's probably best to just do it like this. Save the trampolines and continuations for heavy lifting when a simple loop won't do.setTimeout
is NOT a solution to the stack overflow problemOK, so it does work, but only paradoxically. If your dataset is small, you don't need
setTimeout
because there will be no stack overflow. If your dataset is large and you usesetTimeout
as a safe recursion mechanism, not only do you make it impossible to synchronously return a value from your function, it will be so F slow you won't even want to use your functionSome people have found an interview Q&A prep site that encourages this dreadful strategy
What our
repeat
would look like usingsetTimeout
– notice it's also defined in continuation passing style – ie, we must callrepeat
with a callback (k
) to get the final valueI can't stress enough how bad this is. Even
1e5
takes so long to run that I gave up trying to measure it. I'm not including this in the benchmarks below because it's just too slow to even be considered a viable approach.Promises
Promises have the ability to chain computations and are stack safe. However, achieving a stack-safe
repeat
using Promises means we'll have to give up our synchronous return value, the same way we did usingsetTimeout
. I'm providing this as a "solution" because it does solve the problem, unlikesetTimeout
, but in a way that's very simple compared to the trampoline or continuation monad. As you might imagine though, the performance is somewhat bad, but nowhere near as bad as thesetTimeout
example aboveWorth noting in this solution, the Promise implementation detail is completely hidden from the caller. A single continuation is provided as a 4th argument and its called when the computation is complete.
Benchmarks
Seriously, the
while
loop is a lot faster - like almost 100x faster (when comparing best to worst – but not including async answers:setTimeout
andPromise
)Stone Age JavaScript
The above techniques are demonstrated using newer ES6 syntaxes, but you could implement a trampoline in the earliest possible version of JavaScript – it only requires
while
and first class functionsBelow, we use stone age javascript to demonstrate infinite recursion is possible and performant without necessarily sacrificing a synchronous return value – 100,000,000 recursions in under 6 seconds - this is a dramatic difference compared to
setTimeout
which can only 1,000 recursions in the same amount of time.Non-blocking infinite recursion using stone age JavaScript
If, for some reason, you want non-blocking (asynchronous) infinite recursion, we can rely on
setTimeout
to defer a single frame at the start of the computation. This program also uses stone age javascript and computes 100,000,000 recursions in under 8 seconds, but this time in a non-blocking way.This demonstrates that there's nothing special about having a non-blocking requirement. A
while
loop and first-class functions are still the only fundamental requirement to achieve stack-safe recursion without sacrificing performanceIn a modern program, given Promises, we would substitute the
setTimeout
call for a single Promise.See also unfold which (from Ramda docs)