Is there a mechanism to loop x times in ES6 (ECMAScript 6) without mutable variables?
Using the ES2015 Spread operator:
[...Array(n)].map()
const res = [...Array(10)].map((_, i) => {
return i * 10;
});
// as a one liner
const res = [...Array(10)].map((_, i) => i * 10);
Or if you don't need the result:
[...Array(10)].forEach((_, i) => {
console.log(i);
});
// as a one liner
[...Array(10)].forEach((_, i) => console.log(i));
Or using the ES2015 Array.from operator:
Array.from(...)
const res = Array.from(Array(10)).map((_, i) => {
return i * 10;
});
// as a one liner
const res = Array.from(Array(10)).map((_, i) => i * 10);
Note that if you just need a string repeated you can use String.prototype.repeat.
console.log("0".repeat(10))
// 0000000000
OK!
The code below is written using ES6 syntaxes but could just as easily be written in ES5 or even less. ES6 is not a requirement to create a "mechanism to loop x times"
If you don't need the iterator in the callback, this is the most simple implementation
const times = x => f => {
if (x > 0) {
f()
times (x - 1) (f)
}
}
// use it
times (3) (() => console.log('hi'))
// or define intermediate functions for reuse
let twice = times (2)
// twice the power !
twice (() => console.log('double vision'))
If you do need the iterator, you can use a named inner function with a counter parameter to iterate for you
const times = n => f => {
let iter = i => {
if (i === n) return
f (i)
iter (i + 1)
}
return iter (0)
}
times (3) (i => console.log(i, 'hi'))
Stop reading here if you don't like learning more things ...
But something should feel off about those...
- single branch
if
statements are ugly — what happens on the other branch ? - multiple statements/expressions in the function bodies — are procedure concerns being mixed ?
- implicitly returned
undefined
— indication of impure, side-effecting function
"Isn't there a better way ?"
There is. Let's first revisit our initial implementation
// times :: Int -> (void -> void) -> void
const times = x => f => {
if (x > 0) {
f() // has to be side-effecting function
times (x - 1) (f)
}
}
Sure, it's simple, but notice how we just call f()
and don't do anything with it. This really limits the type of function we can repeat multiple times. Even if we have the iterator available, f(i)
isn't much more versatile.
What if we start with a better kind of function repetition procedure ? Maybe something that makes better use of input and output.
Generic function repetition
// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
if (n > 0)
return repeat (n - 1) (f) (f (x))
else
return x
}
// power :: Int -> Int -> Int
const power = base => exp => {
// repeat <exp> times, <base> * <x>, starting with 1
return repeat (exp) (x => base * x) (1)
}
console.log(power (2) (8))
// => 256
Above, we defined a generic repeat
function which takes an additional input which is used to start the repeated application of a single function.
// repeat 3 times, the function f, starting with x ...
var result = repeat (3) (f) (x)
// is the same as ...
var result = f(f(f(x)))
Implementing times
with repeat
Well this is easy now; almost all of the work is already done.
// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
if (n > 0)
return repeat (n - 1) (f) (f (x))
else
return x
}
// times :: Int -> (Int -> Int) -> Int
const times = n=> f=>
repeat (n) (i => (f(i), i + 1)) (0)
// use it
times (3) (i => console.log(i, 'hi'))
Since our function takes i
as an input and returns i + 1
, this effectively works as our iterator which we pass to f
each time.
We've fixed our bullet list of issues too
- No more ugly single branch
if
statements - Single-expression bodies indicate nicely separated concerns
- No more useless, implicitly returned
undefined
JavaScript comma operator, the
In case you're having trouble seeing how the last example is working, it depends on your awareness of one of JavaScript's oldest battle axes; the comma operator – in short, it evaluates expressions from left to right and returns the value of the last evaluated expression
(expr1 :: a, expr2 :: b, expr3 :: c) :: c
In our above example, I'm using
(i => (f(i), i + 1))
which is just a succinct way of writing
(i => { f(i); return i + 1 })
Tail Call Optimisation
As sexy as the recursive implementations are, at this point it would be irresponsible for me to recommend them given that no JavaScript VM I can think of supports proper tail call elimination – babel used to transpile it, but it's been in "broken; will reimplement" status for well over a year.
repeat (1e6) (someFunc) (x)
// => RangeError: Maximum call stack size exceeded
As such, we should revisit our implementation of repeat
to make it stack-safe.
The code below does use mutable variables n
and x
but note that all mutations are localized to the repeat
function – no state changes (mutations) are visible from outside of the function
// repeat :: Int -> (a -> a) -> (a -> a)
const repeat = n => f => x =>
{
let m = 0, acc = x
while (m < n)
(m = m + 1, acc = f (acc))
return acc
}
// inc :: Int -> Int
const inc = x =>
x + 1
console.log (repeat (1e8) (inc) (0))
// 100000000
This is going to have a lot of you saying "but that's not functional !" – I know, just relax. We can implement a Clojure-style loop
/recur
interface for constant-space looping using pure expressions; none of that while
stuff.
Here we abstract while
away with our loop
function – it looks for a special recur
type to keep the loop running. When a non-recur
type is encountered, the loop is finished and the result of the computation is returned
const recur = (...args) =>
({ type: recur, args })
const loop = f =>
{
let acc = f ()
while (acc.type === recur)
acc = f (...acc.args)
return acc
}
const repeat = $n => f => x =>
loop ((n = $n, acc = x) =>
n === 0
? acc
: recur (n - 1, f (acc)))
const inc = x =>
x + 1
const fibonacci = $n =>
loop ((n = $n, a = 0, b = 1) =>
n === 0
? a
: recur (n - 1, b, a + b))
console.log (repeat (1e7) (inc) (0)) // 10000000
console.log (fibonacci (100)) // 354224848179262000000
for (let i of Array(100).keys()) {
console.log(i)
}