0

I like the functional programming paradigm which according to me produces cleaner code and easier to understand, but I'd like to know if the performance loss is significative or not.

Let's take the example of a function that adds a new property to an object. A non-functional approach would look like this:

const addProp = (obj, prop, val) => {
  obj[prop] = val; // Mutate the object
  return obj;
}

While the functional approach would look like this:

const addProp = (obj, prop, val) => ({ ...obj, [prop]: val }); // Shallow clone

What's the cost of the shallow clone compared to the object mutation? I'd like to know how much functional programming patterns degrade performance (if they do).

Guerric P
  • 30,447
  • 6
  • 48
  • 86
  • 2
    think of what has to happen if the object has 500 properties to begin with. – Pointy Jun 14 '21 at 11:59
  • 3
    Even with 500 properties don't start pre-optimising code. Do tests, only optimise where needed. I highly doubt the few milliseconds saved here will do very much – evolutionxbox Jun 14 '21 at 11:59
  • @Pointy in the case of a shallow clone I think that creating new references on the same objects or cloning primitive values isn't that expensive? Maybe there is a non-negligible overhead related to the new object creation? – Guerric P Jun 14 '21 at 12:02
  • The runtime has to iterate through the original list of properties and then do essentially what your first function does. Or does it? Maybe there's some highly-optimized code that does spreads very fast. The only way to tell is try it. If you write an API that's designed as a black box, you can change it and try one way versus the other. Also, as Joe Armstrong said (many times), you should concentrate on writing the most pleasing code you know how. – Pointy Jun 14 '21 at 12:05
  • 1
    @GuerricP "isn't that expensive" - no work is cheaper than no work. If you do this only a few times an hour, you can disregard the costs. But if you do this in a tight loop or with lots of objects - then it's different. Which is to say, measure measure measure. – Sergio Tulentsev Jun 14 '21 at 12:05
  • https://ericlippert.com/2012/12/17/performance-rant – Mark Seemann Jun 14 '21 at 12:09
  • I agree that there is no better way to figure out about performance than testing, I just wanted to know if there was some general guidance on these kind of patterns. I often feel guilty about writing code like in my second example and I wanted to know if this is justified. – Guerric P Jun 14 '21 at 12:10
  • 3
    It always depends on where you use (call) `addProp`. Do they actually produce the same result? Is it part of a larger, pure function, to which the mutability of `obj` is internal only? Go for correctness first, then universality, then speed. – Bergi Jun 14 '21 at 12:14
  • Possibly related [1](https://gist.github.com/customcommander/97eb4b3f1600773db59406d39f3f9cd7) & [2](https://stackoverflow.com/q/63727586/1244884) – customcommander Jun 14 '21 at 13:01
  • 1
    There is also another way. You can use `Object.create(obj, { [prop]: val })` which makes use of the inheritance instead. For a large set of properties seldom changed that would fare better, but if you do this consecutively the chain will get very long. – Sylwester Jun 14 '21 at 13:17
  • Wow that's a clever idea, though because I am not bothered by simple assignment to a new property I don't think I'd ever do it, but it's good to keep somewhere in my head for a weird situation. – Pointy Jun 14 '21 at 13:29
  • 1
    Yes, there is a general advice for mutations. Use them along with mutable data types, because this is their intended use. If you are concerned about side effects, then these three rules might help: 1) If the composite value already exists, clone it for decoupling from the parent scope. 2) As long as the mutable value is not consumed by another part of the code you can freely mutate it as often as you need. 3) Treat the final value as immutable after its first consumption by another part of the code. If subsequent mutations are required, start over with 1). –  Jun 14 '21 at 13:31

1 Answers1

2

The performance of making shallow copies is much worse than the performance of mutation. If I have an array with 400 elements and I make a shallow copy, that's going to be much more expensive than destructively modifying a single element of the array. And the longer the array is, the worse the gap becomes. The same problem occurs with an object with many properties.

One essential element of efficient functional programming is using the right data structures. In this case, you want a data structure where modifying part of the data structure does not mean you have to completely recreate the data structure. You want a local modification of a data structure to be able to share most of its memory with the original.

An extremely basic example of this is a singly linked list. If you wish to prepend a single element to a linked list, this is an O(1) operation because the new list shares most of its memory with the old list. However, adding a single element to an array without mutating the original array is a linear-time operation because the whole array must be copied.

For sequences and maps, one generally ends up using some sort of balanced tree. Functional programming languages include these data structures as part of their standard libraries, but I'm sure someone has written a library for functional Javascript.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Mark Saving
  • 1,752
  • 7
  • 11