Monday, May 20, 2024
 Popular · Latest · Hot · Upcoming
77
rated 0 times [  81] [ 4]  / answers: 1 / hits: 185440  / 9 Years ago, tue, may 26, 2015, 12:00:00

The typical way to loop x times in JavaScript is:



for (var i = 0; i < x; i++)
doStuff(i);


But I don't want to use the ++ operator or have any mutable variables at all. So is there a way, in ES6, to loop x times another way? I love Ruby's mechanism:



x.times do |i|
do_stuff(i)
end


Anything similar in JavaScript/ES6? I could kind of cheat and make my own generator:



function* times(x) {
for (var i = 0; i < x; i++)
yield i;
}

for (var i of times(5)) {
console.log(i);
}


Of course I'm still using i++. At least it's out of sight :), but I'm hoping there's a better mechanism in ES6.


More From » generator

 Answers
43

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




[#66472] Friday, May 22, 2015, 9 Years  [reply] [flag answer]
Only authorized users can answer the question. Please sign in first, or register a free account.
daquanmilesw

Total Points: 57
Total Questions: 102
Total Answers: 110

Location: Wallis and Futuna
Member since Sat, Aug 6, 2022
2 Years ago
daquanmilesw questions
;