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.
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
const repeat = n => f => x =>
n === 0 ? x : repeat (n - 1) (f) (f(x))
console.log(repeat(1e3) (x => x + 1) (0)) // 1000
console.log(repeat(1e5) (x => x + 1) (0)) // Error: Uncaught RangeError: Maximum call stack size exceeded
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 or Done. Then we will use our trampoline function to handle the loop.
// trampoline
const Bounce = (f,x) => ({ isBounce: true, f, x })
const Done = x => ({ isBounce: false, x })
const trampoline = ({ isBounce, f, x }) => {
while (isBounce)
({ isBounce, f, x } = f(x))
return x
}
// our revised repeat function, now stack-safe
const repeat = n => f => x =>
n === 0 ? Done(x) : Bounce(repeat (n - 1) (f), f(x))
// apply trampoline to the result of an ordinary call repeat
let result = trampoline(repeat(1e6) (x => x + 1) (0))
// no more stack overflow
console.log(result) // 1000000
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
// aux helper hides trampoline implementation detail
// runs about 2x as fast
const repeat = n => f => x => {
const aux = (n, x) =>
n === 0 ? Done(x) : Bounce(x => aux (n - 1, x), f (x))
return trampoline (aux (n, x))
}
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 the Bounce and Done wrappers, too.
Clojure creates a specialised trampoline interface that uses a pair of functions, loop and recur – this tandem pair lends itself to a remarkably elegant expression of a program
Oh and it's really fast, too
const recur = (...values) =>
({ recur, values })
const loop = run =>
{ let r = run ()
while (r && r.recur === recur)
r = run (...r.values)
return r
}
const repeat = n => f => x =>
loop
( (m = n, r = x) =>
m === 0
? r
: recur (m - 1, f (r))
)
console.time ('loop/recur')
console.log (repeat (1e6) (x => x + 1) (0)) // 1000000
console.timeEnd ('loop/recur') // 24 ms
Initially this style will feel foreign, but over time I am finding it to be the most consistent while producing durable programs. Comments below help ease you into the expression-rich syntax -
const repeat = n => f => x =>
loop // begin a loop with
( ( m = n // local loop var m: counter, init with n
, r = x // local loop var r: result, init with x
) =>
m === 0 // terminating condition
? r // return result
: recur // otherwise recur with
( m - 1 // next m value
, f (r) // next r value
)
)
The continuation monad
This is one of my favourite topics tho, so we're gonna see what this looks like with the continuation monad. Reusing loop and recur, we implement a stack-safe cont that can sequence operations using chain and run operation sequences using runCont. For repeat, this is senseless (and slow), but it's cool to see the mechanics of cont at work in this simple example -
const identity = x =>
x
const recur = (...values) =>
({ recur, values })
const loop = run =>
{ let r = run ()
while (r && r.recur === recur)
r = run (...r.values)
return r
}
// cont : 'a -> 'a cont
const cont = x =>
k => recur (k, x)
// chain : ('a -> 'b cont) -> 'a cont -> 'b cont
const chain = f => mx =>
k => recur (mx, x => recur (f (x), k))
// runCont : ('a -> 'b) -> a cont -> 'b
const runCont = f => mx =>
loop ((r = mx, k = f) => r (k))
const repeat = n => f => x =>
{ const aux = n => x =>
n === 0 // terminating condition
? cont (x) // base case, continue with x
: chain // otherise
(aux (n - 1)) // sequence next operation on
(cont (f (x))) // continuation of f(x)
return runCont // run continuation
(identity) // identity; pass-thru
(aux (n) (x)) // the continuation returned by aux
}
console.time ('cont monad')
console.log (repeat (1e6) (x => x + 1) (0)) // 1000000
console.timeEnd ('cont monad') // 451 ms
Y combinator
The 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.
const bounce = f => (...xs) =>
({ isBounce: true, f, xs })
const trampoline = t => {
while (t && t.isBounce)
t = t.f(...t.xs)
return t
}
// stack-safe Y combinator
const Y = f => {
const safeY = f =>
bounce((...xs) => f (safeY (f), ...xs))
return (...xs) =>
trampoline (safeY (f) (...xs))
}
// recur safely to your heart's content
const repeat = Y ((recur, n, f, x) =>
n === 0
? x
: recur (n - 1, f, f (x)))
console.log(repeat (1e5, x => x + 1, 0)) // 10000
Practicality with while loop
But let's be honest: that's a lot of ceremony when we're overlooking one of the more obvious potential solutions: use a for or while loop, but hide it behind a functional interface
For 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 the loop/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 or while 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.
const repeat = n => f => x => {
let m = n
while (true) {
if (m === 0)
return x
else
(m = m - 1, x = f (x))
}
}
const gadzillionTimes = repeat(1e8)
const add1 = x => x + 1
const result = gadzillionTimes (add1) (0)
console.log(result) // 100000000
setTimeout is NOT a solution to the stack overflow problem
OK, 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 use setTimeout 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 function
Some people have found an interview Q&A prep site that encourages this dreadful strategy
What our repeat would look like using setTimeout – notice it's also defined in continuation passing style – ie, we must call repeat with a callback (k) to get the final value
// do NOT implement recursion using setTimeout
const repeat = n => f => x => k =>
n === 0
? k (x)
: setTimeout (x => repeat (n - 1) (f) (x) (k), 0, f (x))
// be patient, this one takes about 5 seconds, even for just 1000 recursions
repeat (1e3) (x => x + 1) (0) (console.log)
// comment the next line out for absolute madness
// 10,000 recursions will take ~1 MINUTE to complete
// paradoxically, direct recursion can compute this in a few milliseconds
// setTimeout is NOT a fix for the problem
// -----------------------------------------------------------------------------
// repeat (1e4) (x => x + 1) (0) (console.log)
I 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 using setTimeout. I'm providing this as a "solution" because it does solve the problem, unlike setTimeout, 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 the setTimeout example above
Worth 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.
const repeat = n => f => x => k =>
n === 0
? Promise.resolve(x).then(k)
: Promise.resolve(f(x)).then(x => repeat (n - 1) (f) (x) (k))
// be patient ...
repeat (1e6) (x => x + 1) (0) (x => console.log('done', x))
Benchmarks
Seriously, the while loop is a lot faster - like almost 100x faster (when comparing best to worst – but not including async answers: setTimeout and Promise)
// sync
// -----------------------------------------------------------------------------
// repeat implemented with basic trampoline
console.time('A')
console.log(tramprepeat(1e6) (x => x + 1) (0))
console.timeEnd('A')
// 1000000
// A 114 ms
// repeat implemented with basic trampoline and aux helper
console.time('B')
console.log(auxrepeat(1e6) (x => x + 1) (0))
console.timeEnd('B')
// 1000000
// B 64 ms
// repeat implemented with cont monad
console.time('C')
console.log(contrepeat(1e6) (x => x + 1) (0))
console.timeEnd('C')
// 1000000
// C 33 ms
// repeat implemented with Y
console.time('Y')
console.log(yrepeat(1e6) (x => x + 1) (0))
console.timeEnd('Y')
// 1000000
// Y 544 ms
// repeat implemented with while loop
console.time('D')
console.log(whilerepeat(1e6) (x => x + 1) (0))
console.timeEnd('D')
// 1000000
// D 4 ms
// async
// -----------------------------------------------------------------------------
// repeat implemented with Promise
console.time('E')
promiserepeat(1e6) (x => x + 1) (0) (console.log)
console.timeEnd('E')
// 1000000
// E 2224 ms
// repeat implemented with setTimeout; FAILED
console.time('F')
timeoutrepeat(1e6) (x => x + 1) (0) (console.log)
console.timeEnd('F')
// ...
// too slow; didn't finish after 3 minutes
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 functions
Below, 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.
function trampoline (t) {
while (t && t.isBounce)
t = t.f (t.x);
return t.x;
}
function bounce (f, x) {
return { isBounce: true, f: f, x: x };
}
function done (x) {
return { isBounce: false, x: x };
}
function repeat (n, f, x) {
function aux (n, x) {
if (n === 0)
return done (x);
else
return bounce (function (x) { return aux (n - 1, x); }, f (x));
}
return trampoline (aux (n, x));
}
console.time('JS1 100K');
console.log (repeat (1e5, function (x) { return x + 1 }, 0));
console.timeEnd('JS1 100K');
// 100000
// JS1 100K: 15ms
console.time('JS1 100M');
console.log (repeat (1e8, function (x) { return x + 1 }, 0));
console.timeEnd('JS1 100M');
// 100000000
// JS1 100K: 5999ms
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 performance
In a modern program, given Promises, we would substitute the setTimeout call for a single Promise.
function donek (k, x) {
return { isBounce: false, k: k, x: x };
}
function bouncek (f, x) {
return { isBounce: true, f: f, x: x };
}
function trampolinek (t) {
// setTimeout is called ONCE at the start of the computation
// NOT once per recursion
return setTimeout(function () {
while (t && t.isBounce) {
t = t.f (t.x);
}
return t.k (t.x);
}, 0);
}
// stack-safe infinite recursion, non-blocking, 100,000,000 recursions in under 8 seconds
// now repeatk expects a 4th-argument callback which is called with the asynchronously computed result
function repeatk (n, f, x, k) {
function aux (n, x) {
if (n === 0)
return donek (k, x);
else
return bouncek (function (x) { return aux (n - 1, x); }, f (x));
}
return trampolinek (aux (n, x));
}
console.log('non-blocking line 1')
console.time('non-blocking JS1')
repeatk (1e8, function (x) { return x + 1; }, 0, function (result) {
console.log('non-blocking line 3', result)
console.timeEnd('non-blocking JS1')
})
console.log('non-blocking line 2')
// non-blocking line 1
// non-blocking line 2
// [ synchronous program stops here ]
// [ below this line, asynchronous program continues ]
// non-blocking line 3 100000000
// non-blocking JS1: 7762ms
A better loop/recur pattern
There are two things that I dislike about the loop/recur pattern described in the accepted answer. Note that I actually like the idea behind the loop/recur pattern. However, I dislike the way it has been implemented. So, let's first look at the way I would have implemented it.
// Recur :: a -> Result a b
const Recur = (...args) => ({ recur: true, args });
// Return :: b -> Result a b
const Return = value => ({ recur: false, value });
// loop :: (a -> Result a b) -> a -> b
const loop = func => (...args) => {
let result = func(...args);
while (result.recur) result = func(...result.args);
return result.value;
};
// repeat :: (Int, a -> a, a) -> a
const repeat = loop((n, f, x) => n === 0 ? Return(x) : Recur(n - 1, f, f(x)));
console.time("loop/recur/return");
console.log(repeat(1e6, x => x + 1, 0));
console.timeEnd("loop/recur/return");
Compare this with the loop/recur pattern described in the aforementioned answer.
// recur :: a -> Recur a
const recur = (...args) => ({ recur, args });
// loop :: (a? -> Recur a ∪ b) -> b
const loop = func => {
let result = func();
while (result && result.recur === recur) result = func(...result.args);
return result;
};
// repeat :: (Int, a -> a, a) -> a
const repeat = (n, f, x) => loop((m = n, r = x) => m === 0 ? r : recur(m - 1, f(r)));
console.time("loop/recur");
console.log(repeat(1e6, x => x + 1, 0));
console.timeEnd("loop/recur");
If you notice, the type signature of the second loop function uses default parameters (i.e. a?) and untagged unions (i.e. Recur a ∪ b). Both of these features are at odds with the functional programming paradigm.
Problem with default parameters
The loop/recur pattern in the aforementioned answer uses default parameters to supply the initial arguments of the function. I think this is an abuse of default parameters. You can just as easily supply initial arguments using my version of loop.
// repeat :: (Int, a -> a, a) -> a
const repeat = (n, f, x) => loop((n, x) => n === 0 ? Return(x) : Recur(n - 1, f(x)))(n, x);
// or more readable
const repeat = (n, f, x) => {
const repeatF = loop((n, x) => n === 0 ? Return(x) : Recur(n - 1, f(x)));
return repeatF(n, x);
};
Futhermore, it allows eta conversion when the all the arguments are passed through.
// repeat :: (Int, a -> a, a) -> a
const repeat = (n, f, x) => loop((n, f, x) => n === 0 ? Return(x) : Recur(n - 1, f, f(x)))(n, f, x);
// can be η-converted to
const repeat = loop((n, f, x) => n === 0 ? Return(x) : Recur(n - 1, f, f(x)));
Using the version of loop with default parameters does not allow eta conversion. In addition, it forces you to rename parameters because you can't write (n = n, x = x) => ... in JavaScript.
Problem with untagged unions
Untagged unions are bad because they erase important information, i.e. information of where the data came from. For example, because my Result type is tagged I can distinguish Return(Recur(0)) from Recur(0).
On the other hand, because the right-hand side variant of Recur a ∪ b is untagged, if b is specialized to Recur a, i.e. if the type is specialized to Recur a ∪ Recur a, then it's impossible to determine whether the Recur a came from the left-hand side or the right-hand side.
One criticism might be that b will never be specialized to Recur a, and hence it doesn't matter that b is untagged. Here's a simple counterexample to that criticism.
// recur :: a -> Recur a
const recur = (...args) => ({ recur, args });
// loop :: (a? -> Recur a ∪ b) -> b
const loop = func => {
let result = func();
while (result && result.recur === recur) result = func(...result.args);
return result;
};
// repeat :: (Int, a -> a, a) -> a
const repeat = (n, f, x) => loop((m = n, r = x) => m === 0 ? r : recur(m - 1, f(r)));
// infinite loop
console.log(repeat(1, x => recur(1, x), "wow, such hack, much loop"));
// unreachable code
console.log("repeat wasn't hacked");
Compare this with my version of repeat which is bulletproof.
// Recur :: a -> Result a b
const Recur = (...args) => ({ recur: true, args });
// Return :: b -> Result a b
const Return = value => ({ recur: false, value });
// loop :: (a -> Result a b) -> a -> b
const loop = func => (...args) => {
let result = func(...args);
while (result.recur) result = func(...result.args);
return result.value;
};
// repeat :: (Int, a -> a, a) -> a
const repeat = loop((n, f, x) => n === 0 ? Return(x) : Recur(n - 1, f, f(x)));
// finite loop
console.log(repeat(1, x => Recur(1, x), "wow, such hack, much loop"));
// reachable code
console.log("repeat wasn't hacked");
Thus, untagged unions are unsafe. However, even if we were careful to avoid the pitfalls of untagged unions I would still prefer tagged unions because the tags provide useful information when reading and debugging the program. IMHO, the tags make the program more understandable and easier to debug.
Conclusion
To quote the Zen of Python.
Explicit is better than implicit.
Default parameters and untagged unions are bad because they are implicit, and can lead to ambiguities.
The Trampoline monad
Now, I'd like to switch gears and talk about monads. The accepted answer demonstrates a stack-safe continuation monad. However, if you only need to create a monadic stack-safe recursive function then you don't need the full power of the continuation monad. You can use the Trampoline monad.
The Trampoline monad is a more powerful cousin of the Loop monad, which is just the loop function converted into a monad. So, let's start by understanding the Loop monad. Then we'll see the main problem of the Loop monad and how the Trampoline monad can be used to fix that problem.
// Recur :: a -> Result a b
const Recur = (...args) => ({ recur: true, args });
// Return :: b -> Result a b
const Return = value => ({ recur: false, value });
// Loop :: (a -> Result a b) -> a -> Loop b
const Loop = func => (...args) => ({ func, args });
// runLoop :: Loop a -> a
const runLoop = ({ func, args }) => {
let result = func(...args);
while (result.recur) result = func(...result.args);
return result.value;
};
// pure :: a -> Loop a
const pure = Loop(Return);
// bind :: (Loop a, a -> Loop b) -> Loop b
const bind = (loop, next) => Loop(({ first, loop: { func, args } }) => {
const result = func(...args);
if (result.recur) return Recur({ first, loop: { func, args: result.args } });
if (first) return Recur({ first: false, loop: next(result.value) });
return result;
})({ first: true, loop });
// ack :: (Int, Int) -> Loop Int
const ack = (m, n) => {
if (m === 0) return pure(n + 1);
if (n === 0) return ack(m - 1, 1);
return bind(ack(m, n - 1), n => ack(m - 1, n));
};
console.log(runLoop(ack(3, 4)));
Note that loop has been split into a Loop and a runLoop function. The data structure returned by Loop is a monad, and the pure and bind functions implement its monadic interface. We use the pure and bind functions to write a straightforward implementation of the Ackermann function.
Unfortunately, the ack function is not stack safe because it recursively calls itself until it reaches a pure value. Instead, we would like ack to return a Recur like data structure for its inductive cases. However, Recur values are of type Result instead of Loop. This problem is solved by the Trampoline monad.
// Bounce :: (a -> Trampoline b) -> a -> Trampoline b
const Bounce = func => (...args) => ({ bounce: true, func, args });
// Return :: a -> Trampoline a
const Return = value => ({ bounce: false, value });
// trampoline :: Trampoline a -> a
const trampoline = result => {
while (result.bounce) result = result.func(...result.args);
return result.value;
};
// pure :: a -> Trampoline a
const pure = Return;
// bind :: (Trampoline a, a -> Trampoline b) -> Trampoline b
const bind = (first, next) => first.bounce ?
Bounce(args => bind(first.func(...args), next))(first.args) :
next(first.value);
// ack :: (Int, Int) -> Trampoline Int
const ack = Bounce((m, n) => {
if (m === 0) return pure(n + 1);
if (n === 0) return ack(m - 1, 1);
return bind(ack(m, n - 1), n => ack(m - 1, n));
});
console.log(trampoline(ack(3, 4)));
The Trampoline data type is a combination of Loop and Result. The Loop and Recur data constructors have been combined into a single Bounce data constructor. The runLoop function has been simplified and renamed to trampoline. The pure and bind functions have also been simplified. In fact, pure is just Return. Finally, we apply Bounce to the original implementation of the ack function.
Another advantage of Trampoline is that it can be used to define stack-safe mutually recursive functions. For example, here's an implementation of the Hofstadter Female and Male sequence functions.
// Bounce :: (a -> Trampoline b) -> a -> Trampoline b
const Bounce = func => (...args) => ({ bounce: true, func, args });
// Return :: a -> Trampoline a
const Return = value => ({ bounce: false, value });
// trampoline :: Trampoline a -> a
const trampoline = result => {
while (result.bounce) result = result.func(...result.args);
return result.value;
};
// pure :: a -> Trampoline a
const pure = Return;
// bind :: (Trampoline a, a -> Trampoline b) -> Trampoline b
const bind = (first, next) => first.bounce ?
Bounce(args => bind(first.func(...args), next))(first.args) :
next(first.value);
// female :: Int -> Trampoline Int
const female = Bounce(n => n === 0 ? pure(1) :
bind(female(n - 1), f =>
bind(male(f), m =>
pure(n - m))));
// male :: Int -> Trampoline Int
const male = Bounce(n => n === 0 ? pure(0) :
bind(male(n - 1), m =>
bind(female(m), f =>
pure(n - f))));
console.log(Array.from({ length: 21 }, (_, n) => trampoline(female(n))).join(" "));
console.log(Array.from({ length: 21 }, (_, n) => trampoline(male(n))).join(" "));
The major pain point of writing monadic code is callback hell. However, this can be solved using generators.
// Bounce :: (a -> Trampoline b) -> a -> Trampoline b
const Bounce = func => (...args) => ({ bounce: true, func, args });
// Return :: a -> Trampoline a
const Return = value => ({ bounce: false, value });
// trampoline :: Trampoline a -> a
const trampoline = result => {
while (result.bounce) result = result.func(...result.args);
return result.value;
};
// pure :: a -> Trampoline a
const pure = Return;
// bind :: (Trampoline a, a -> Trampoline b) -> Trampoline b
const bind = (first, next) => first.bounce ?
Bounce(args => bind(first.func(...args), next))(first.args) :
next(first.value);
// bounce :: (a -> Generator (Trampoline b)) -> a -> Trampoline b
const bounce = func => Bounce((...args) => {
const gen = func(...args);
const next = data => {
const { value, done } = gen.next(data);
return done ? value : bind(value, next);
};
return next(undefined);
});
// female :: Int -> Trampoline Int
const female = bounce(function* (n) {
return pure(n ? n - (yield male(yield female(n - 1))) : 1);
});
// male :: Int -> Trampoline Int
const male = bounce(function* (n) {
return pure(n ? n - (yield female(yield male(n - 1))) : 0);
});
console.log(Array.from({ length: 21 }, (_, n) => trampoline(female(n))).join(" "));
console.log(Array.from({ length: 21 }, (_, n) => trampoline(male(n))).join(" "));
Finally, mutually recursive functions also demonstrate the advantage of having a separate trampoline function. It allows us to call a function returning a Trampoline value without actually running it. This allows us to build bigger Trampoline values, and then run the entire computation when required.
Conclusion
If you're want to write indirectly or mutually recursive stack-safe functions, or monadic stack-safe functions then use the Trampoline monad. If you want to write non-monadic directly recursive stack-safe functions then use the loop/recur/return pattern.
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:
base case
recursive case
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
// simulate a tagged union with two Object types
const Loop = x =>
({value: x, done: false});
const Done = x =>
({value: x, done: true});
// trampoline
const tailRec = f => (...args) => {
let step = Loop(args);
do {
step = f(Loop, Done, step.value);
} while (!step.done);
return step.value;
};
// stack-safe function
const repeat = n => f => x =>
tailRec((Loop, Done, [m, y]) => m === 0
? Done(y)
: Loop([m - 1, f(y)])) (n, x);
// run...
const inc = n =>
n + 1;
console.time();
console.log(repeat(1e6) (inc) (0));
console.timeEnd();
Function Encoded
Alternatively, we can create a real tagged union with a function encoding. Now our style is much closer to mature functional languages:
// type/data constructor
const Type = Tcons => (tag, Dcons) => {
const t = new Tcons();
t.run = cases => Dcons(cases);
t.tag = tag;
return t;
};
// tagged union specific for the case
const Step = Type(function Step() {});
const Done = x =>
Step("Done", cases => cases.Done(x));
const Loop = args =>
Step("Loop", cases => cases.Loop(args));
// trampoline
const tailRec = f => (...args) => {
let step = Loop(args);
do {
step = f(step);
} while (step.tag === "Loop");
return step.run({Done: id});
};
// stack-safe function
const repeat = n => f => x =>
tailRec(step => step.run({
Loop: ([m, y]) => m === 0 ? Done(y) : Loop([m - 1, f(y)]),
Done: y => Done(y)
})) (n, x);
// run...
const inc = n => n + 1;
const id = x => x;
console.log(repeat(1e6) (inc) (0));
See also unfold which (from Ramda docs)
Builds a list from a seed value. Accepts an iterator function, which
returns either false to stop iteration or an array of length 2
containing the value to add to the resulting list and the seed to be
used in the next call to the iterator function.
var r = n => f => x => x > n ? false : [x, f(x)];
var repeatUntilGreaterThan = n => f => R.unfold(r(n)(f), 1);
console.log(repeatUntilGreaterThan(10)(x => x + 1));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.22.1/ramda.min.js"></script>
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.
"some string "+(a=>{
while(comparison){
// run code
}
return result;
})(somearray)+" some more"
Alternatively, if what you want is something that chains off an array, than you can use the reduce method.
somearray.reduce((r,o,i,a)=>{
while(comparison){
// run code
}
a.splice(1); // This would ensure only one call.
return result;
},[])+" some more"
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.
I love currying but there are a couple of reasons why a lof of Javascript devs reject this technique:
aesthetic concerns about the typical curry pattern: f(x) (y) (z)
concerns about performance penalties due to the increased number of function calls
concerns about debugging issues because of the many nested anonymous functions
concerns about readability of point-free style (currying in connection with composition)
Is there an approach that can mitigate these concerns so that my coworkers don't hate me?
Note: #ftor answered his/her own question. This is a direct companion to that answer.
You're already a genius
I think you might've re-imagined the partial function – at least, in part!
const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);
and it's counter-part, partialRight
const partialRight = (f, ...xs) => (...ys) => f(...ys, ...xs);
partial takes a function, some args (xs), and always returns a function that takes some more args (ys), then applies f to (...xs, ...ys)
Initial remarks
The context of this question is set in how currying and composition can play nice with a large user base of coders. My remarks will be in the same context
just because a function may return a function does not mean that it is curried – tacking on a _ to signify that a function is waiting for more args is confusing. Recall that currying (or partial function application) abstracts arity, so we never know when a function call will result in the value of a computation or another function waiting to be called.
curry should not flip arguments; that is going to cause some serious wtf moments for your fellow coder
if we're going to create a wrapper for reduce, the reduceRight wrapper should be consistent – eg, your foldl uses f(acc, x, i) but your foldr uses f(x, acc, i) – this will cause a lot of pain amongst coworkers that aren't familiar with these choices
For the next section, I'm going to replace your composable with partial, remove _-suffixes, and fix the foldr wrapper
Composable functions
const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);
const partialRight = (f, ...xs) => (...ys) => f(...ys, ...xs);
const comp = (f, g) => x => f(g(x));
const foldl = (f, acc, xs) => xs.reduce(f, acc);
const drop = (xs, n) => xs.slice(n);
const add = (x, y) => x + y;
const sum = partial(foldl, add, 0);
const dropAndSum = comp(sum, partialRight(drop, 1));
console.log(
dropAndSum([1,2,3,4]) // 9
);
Programmatic solution
const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);
// restore consistent interface
const foldr = (f, acc, xs) => xs.reduceRight(f, acc);
const comp = (f,g) => x => f(g(x));
// added this for later
const flip = f => (x,y) => f(y,x);
const I = x => x;
const inc = x => x + 1;
const compn = partial(foldr, flip(comp), I);
const inc3 = compn([inc, inc, inc]);
console.log(
inc3(0) // 3
);
A more serious task
const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);
const filter = (f, xs) => xs.filter(f);
const comp2 = (f, g, x, y) => f(g(x, y));
const len = xs => xs.length;
const odd = x => x % 2 === 1;
const countWhere = f => partial(comp2, len, filter, f);
const countWhereOdd = countWhere(odd);
console.log(
countWhereOdd([1,2,3,4,5]) // 3
);
Partial power !
partial can actually be applied as many times as needed
const partial = (f, ...xs) => (...ys) => f(...xs, ...ys)
const p = (a,b,c,d,e,f) => a + b + c + d + e + f
let f = partial(p,1,2)
let g = partial(f,3,4)
let h = partial(g,5,6)
console.log(p(1,2,3,4,5,6)) // 21
console.log(f(3,4,5,6)) // 21
console.log(g(5,6)) // 21
console.log(h()) // 21
This makes it an indispensable tool for working with variadic functions, too
const partial = (f, ...xs) => (...ys) => f(...xs, ...ys)
const add = (x,y) => x + y
const p = (...xs) => xs.reduce(add, 0)
let f = partial(p,1,1,1,1)
let g = partial(f,2,2,2,2)
let h = partial(g,3,3,3,3)
console.log(h(4,4,4,4))
// 1 + 1 + 1 + 1 +
// 2 + 2 + 2 + 2 +
// 3 + 3 + 3 + 3 +
// 4 + 4 + 4 + 4 => 40
Lastly, a demonstration of partialRight
const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);
const partialRight = (f, ...xs) => (...ys) => f(...ys, ...xs);
const p = (...xs) => console.log(...xs)
const f = partialRight(p, 7, 8, 9);
const g = partial(f, 1, 2, 3);
const h = partial(g, 4, 5, 6);
p(1, 2, 3, 4, 5, 6, 7, 8, 9) // 1 2 3 4 5 6 7 8 9
f(1, 2, 3, 4, 5, 6) // 1 2 3 4 5 6 7 8 9
g(4, 5, 6) // 1 2 3 4 5 6 7 8 9
h() // 1 2 3 4 5 6 7 8 9
Summary
OK, so partial is pretty much a drop in replacement for composable but also tackles some additional corner cases. Let's see how this bangs up against your initial list
aesthetic concerns: avoids f (x) (y) (z)
performance: unsure, but i suspect performance is about the same
debugging: still an issue because partial creates new functions
readability: i think readability here is pretty good, actually. partial is flexible enough to remove points in many cases
I agree with you that there's no replacement for fully curried functions. I personally found it easy to adopt the new style once I stopped being judgmental about the "ugliness" of the syntax – it's just different and people don't like different.
The current prevailing approach provides that each multi argument function is wrapped in a dynamic curry function. While this helps with concern #1, it leaves the remaining ones untouched. Here is an alternative approach.
Composable functions
A composable function is curried only in its last argument. To distinguish them from normal multi argument functions, I name them with a trailing underscore (naming is hard).
const comp_ = (f, g) => x => f(g(x)); // composable function
const foldl_ = (f, acc) => xs => xs.reduce((acc, x, i) => f(acc, x, i), acc);
const curry = f => y => x => f(x, y); // fully curried function
const drop = (xs, n) => xs.slice(n); // normal, multi argument function
const add = (x, y) => x + y;
const sum = foldl_(add, 0);
const dropAndSum = comp_(sum, curry(drop) (1));
console.log(
dropAndSum([1,2,3,4]) // 9
);
With the exception of drop, dropAndSum consists exclusively of multi argument or composable functions and yet we've achieved the same expressiveness as with fully curried functions - at least with this example.
You can see that each composable function expects either uncurried or other composable functions as arguments. This will increase speed especially for iterative function applications. However, this is also restrictive as soon as the result of a composable function is a function again. Look into the countWhere example below for more information.
Programmatic solution
Instead of defining composable functions manually we can easily implement a programmatic solution:
// generic functions
const composable = f => (...args) => x => f(...args, x);
const foldr = (f, acc, xs) =>
xs.reduceRight((acc, x, i) => f(x, acc, i), acc);
const comp_ = (f, g) => x => f(g(x));
const I = x => x;
const inc = x => x + 1;
// derived functions
const foldr_ = composable(foldr);
const compn_ = foldr_(comp_, I);
const inc3 = compn_([inc, inc, inc]);
// and run...
console.log(
inc3(0) // 3
);
Operator functions vs. higher order functions
Maybe you noticed that curry (form the first example) swaps arguments, while composable does not. curry is meant to be applied to operator functions like drop or sub only, which have a different argument order in curried and uncurried form respectively. An operator function is any function that expects only non-functional arguments. In this sence...
const I = x => x;
const eq = (x, y) => x === y; // are operator functions
// whereas
const A = (f, x) => f(x);
const U = f => f(f); // are not operator but a higher order functions
Higher order functions (HOFs) don't need swapped arguments but you will regularly encounter them with arities higher than two, hence the composbale function is useful.
HOFs are one of the most awesome tools in functional programming. They abstract from function application. This is the reason why we use them all the time.
A more serious task
We can solve more complex tasks as well:
// generic functions
const composable = f => (...args) => x => f(...args, x);
const filter = (f, xs) => xs.filter(f);
const comp2 = (f, g, x, y) => f(g(x, y));
const len = xs => xs.length;
const odd = x => x % 2 === 1;
// compositions
const countWhere_ = f => composable(comp2) (len, filter, f); // (A)
const countWhereOdd = countWhere_(odd);
// and run...
console.log(
countWhereOdd([1,2,3,4,5]) // 3
);
Please note that in line A we were forced to pass f explicitly. This is one of the drawbacks of composable against curried functions: Sometimes we need to pass the data explicitly. However, if you dislike point-free style, this is actually an advantage.
Conclusion
Making functions composable mitigates the following concerns:
aesthetic concerns (less frequent use of the curry pattern f(x) (y) (z)
performance penalties (far fewer function calls)
However, point #4 (readability) is only slightly improved (less point-free style) and point #3 (debugging) not at all.
While I am convinced that a fully curried approach is superior to the one presented here, I think composable higher order functions are worth thinking about. Just use them as long as you or your coworkers don't feel comfortable with proper currying.