Search code examples
javascriptgeneratorecmascript-6ecmascript-harmony

Is there a mechanism to loop x times in ES6 (ECMAScript 6) without mutable variables?


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.


Solution

  • 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