2

I am trying to re-implement redux compose function, instead of using reduce i use a for loop, here is my code:

function compose(...funcs) {
  if (funcs.length === 0) {
    return (arg) => arg;
  }

  if (funcs.length === 1) {
    return funcs[0];
  }

  let result;
  for (let i = funcs.length - 1; i > -1; i--) {
    result = result
      ? (...args) => funcs[i](result(...args))
      : (...args) => funcs[i](...args);
  }

  return result;
}

// test
function fn1(x) {
  return x + 1;
}

function fn2(x) {
  return x * 10;
}

function fn3(x) {
  return x - 1;
}

console.log(compose(fn3, fn2, fn1)(10)); // 109

It is expected to log 109 since (10 + 1) * 10 - 1 is 109, however it gives me such error:

RangeError: Maximum call stack size 

Looks like i am doing some recursion but all i did is just a for loop, no sure where is the problem of my code?

Limboer
  • 373
  • 4
  • 24

2 Answers2

1

I think the issue is like the below example:

a = () => 2;
a = () => 3 * a();
console.log(a); 
// this prints () => 3 * a() in console
// so when you call a(), it will call 3 * a(), which will again call 3 * a() and so on
// leading to infinite recursion

My solution is slightly different using bind function based on this reference link: https://stackoverflow.com/a/6772648/4688321.

I think bind creates a new copy of the function result and binds it to a new object. Not using bind leads to recursion because then the code becomes like the above example, result calls result.

function compose(...funcs) {
  if (funcs.length === 0) {
    return (arg) => arg;
  }

  if (funcs.length === 1) {
    return funcs[0];
  }

  let result;
  for (let i = funcs.length - 1; i > -1; i--) {
    if (i == funcs.length - 1)
      result = (...args) => funcs[i](...args);
    else {
      let temp = result.bind({});
      result = (...args) => funcs[i](temp(...args));
    }
  }
  return result;
}

// test
function fn1(x) {
  console.log("fn1");
  return x + 1;
}

function fn2(x) {
  console.log("fn2");
  return x * 10;
}

function fn3(x) {
  console.log("fn3");
  return x - 1;
}

//console.log(compose(fn3, fn2, fn1));
let ret = compose(fn3, fn2, fn1);
console.log(ret(10)); // 109
kiner_shah
  • 3,939
  • 7
  • 23
  • 37
  • 1
    Cheers, i see the problem. In your first example, when declaring the `a = () => 3 * a()` here `a` actual equals `() => 2`, however right now `a` does not get executed. After call `a()`, `a` get executed but because of the above declaration, a becomes `() => 3 * a()` and leads to a infinite recursion. – Limboer Jan 20 '22 at 11:59
  • 1
    Glad to help :-) – kiner_shah Jan 20 '22 at 12:00
1

Rather than trying to combine functions at the time of compose, it seems much easier to combine them at the time the resulting function is called:

function compose(...funcs) {
  if (funcs.length === 0) {
    return (arg) => arg
  }

  return function (...args) {
    let result = funcs .at (-1) (...args)
    for (let i = funcs.length - 2; i > -1; i--) {
      result = funcs [i] (result)
    }
    return result
  }
}

// test
function fn1(x) {
  return x + 1;
}

function fn2(x) {
  return x * 10;
}

function fn3(x) {
  return x - 1;
}

console.log(compose(fn3, fn2, fn1)(10)); // 109

However, again, reduce make for a much cleaner implementation:

const compose = (...fns) => (arg) => 
  fns .reduceRight ((a, fn) => fn (a), arg)

or if you want to allow the rightmost function to receive multiple variables, then

const compose = (...fns) => (...args) => 
  fns .reduceRight ((a, fn) => [fn (...a)], args) [0]
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103