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?
-
\$\begingroup\$ You may have a look at my answer under the same question It uses ES6 though. \$\endgroup\$Redu– Redu2018年04月29日 10:16:56 +00:00Commented Apr 29, 2018 at 10:16
2 Answers 2
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))
})
}
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.
-
\$\begingroup\$ Interesting take on
concat
.. Wasn't thinking of it in terms of the cost of usingconcat
.. 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\$Chirag Ravindra– Chirag Ravindra2018年04月09日 12:32:46 +00:00Commented Apr 9, 2018 at 12:32
Explore related questions
See similar questions with these tags.