2
\$\begingroup\$

I posted an answer on StackOverflow which I believed to be adherent to the principles of Functional Programming.

However, I was told by the original asker that it was not 'functional' as my function used an internal variable oldData which kept track of results.

I believe that the code still satisfies the paradigms of functional programming as it does not mutate it's arguments, does not use globals and has no side effects (assuming action is not a network call)

Is the function process() violating the principles of functional programming? If so, how would I fix it?

var items = [
    ["item1", "item2"],
    ["item3", "item4"],
    ["item5", "item6"],
    ["item7", "item8"],
    ["item9", "item10"]
]

function action(item) {
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            resolve(item + ":processed");
        }, 100)
    });
}

function process(items) {
  return items.reduce((m, d) => {
    const promises = d.map(i => action(i));
    let oldData;
    return m.then((data) => {
        oldData = data;
        return Promise.all(promises);
      })
      .then(values => {
        //oldData.push(...values);
        oldData.push.apply(oldData, values);
        return Promise.resolve(oldData);
      })
  }, Promise.resolve([]))
}

process(items).then(d => console.log(d))

//Prints:

// ["item1:processed","item2:processed","item3:processed","item4:processed","item5:processed","item6:processed","item7:processed","item8:processed","item9:processed","item10:processed"]

The original asker suggested that I update my code to use concat instead of push to create immutable arrays every time to make this properly functional. Does that make sense?

\$\endgroup\$
1

2 Answers 2

2
\$\begingroup\$

Does this function break the paradigm of functional programming in JS?

Short answer: It depends

One common response I get from Software Engineering SE on this topic is you don't have to be pure all the time. A function can still act functional even if the implementation isn't written in a functional manner. Take for example the following:

// All return an array with numbers starting from s to e with no
// side-effects and takes all input from args. The first one is
// obviously not "functional" but works just as well as the others.

function range(s, e){
  const array = []
  for(let i = s; i < e; i++){
    array.push(s)
  }
  return array
}

function range(s, e){
  return Array(e - l).fill(null).map((v, i) => s + i)
}

function range(s, e){
  return s == e ? [] : [s, ...range(s + 1, e)]
}

Striking a balance between ideal and practical is also a deciding factor. For instance, recursion is not a foreign concept in JS. But historically, due to stack size limits, loops became more prevalent. You can safely assume people can read loops better than recursion, and are therefore more likely to understand the first sample than the other two.


It looks like the purpose of process function is to batch-process arrays of items. This can be done with recursion. The idea is to process the current item, then concat the results of the next call, which does the same thing until there's nothing in the array. If you use Node or have Babel to transpile, you can use async/await to simplify the syntax.

const process = async (i) => {
  if (!i.length) return []

  const r1 = await Promise.all(i[0].map(action))
  const r2 = await process(i.slice(1))

  return [...r1, ...r2]
}

If you can't do async/await, here's an expanded version with regular promise syntax.

function process(i) {
  if (!i.length) return Promise.resolve([]) 

  return Promise.all(i[0].map(action)).then(function(r1) {
    return process(i.slice(1)).then(r2 => r1.concat(r2))
  })
}
\$\endgroup\$
1
\$\begingroup\$

I believe your version has a potential memory leak. Given you're able to do what you did with oldData, anybody can do the same, e.g.

let x
process(items).then(d => x = d)

You can get away with creating a copy of the result at the end, i.e.

function process(xs) {
  return xs.reduce(...).then(res => res.slice());
}

But the point about immutable arrays sounds a bit ridiculous to me. As the array grows, so does the cost of using concat; whereas push is virtually constant time. Having state that does not leak out is totally fine, otherwise your functional code, as good looking as it is, would have poor performance.

\$\endgroup\$
1
  • \$\begingroup\$ Interesting take on concat.. Wasn't thinking of it in terms of the cost of using concat.. And now that you mention it, it makes perfect sense.. I'm a little confused as to how it could cause a memory leak.. Even if the reference to the created array is is assigned to some variable outside, it won't be a leak as everytime the function is called, a new array is created and the reference lost after the function returns (unless reassigned). Wouldn't that tell the GC to clean it up? \$\endgroup\$ Apr 9, 2018 at 12:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.