DEV Community

Cover image for Pure Critique of Pure Functions in Functional Programming
Yeom suyun
Yeom suyun

Posted on

Pure Critique of Pure Functions in Functional Programming

TL;DR

A few days ago, I came across an article with an intriguing title on the internet.
"Clean" Code, Horrible Performance
Wow, that title definitely leaves no other option but to click and find out more!
I immediately delved into the article, and to summarize its content, it discussed how ignoring some key principles of clean code actually resulted in code performance improvements ranging from 1.5 times to more than 10 times faster, ultimately arriving at the same conclusion as the title suggests.
However, there are some logical errors in the article's argument. One of them is that even if a benchmark makes something 10 times faster, it doesn't necessarily mean the overall program's performance will be 10 times faster.
For example, looking at this benchmark, using functions in JavaScript might be 8 times faster than using classes, but in most programs, we would hardly notice the difference between these two approaches.
Furthermore, modern developers tend to prioritize development cost over a certain level of performance and maintenance cost over development cost, which is a perspective that many consider to be valid.
However, not all paradigms involve a trade-off between performance and developer experience, and i'll say the argument is made that functional programming not only hinders performance but also negatively impacts the developer experience in this article.

Pipeline and Performance

Using pure functions and immutability to create pipelines is indeed the essence of functional programming and the core principle that underlies it.
Paradoxically, however, functional programming can introduce significant overhead when using pipelines.
Let's look at the following example.

function pipe(...funcs) {
  return function(data) {
    for (const func of funcs) {
      data = func(data)
    }
    return data
  }
}
const data = { ...somethings }
const new_data = pipe(
  function_1,
  function_2,
  function_3,
  function_4,
  function_5
)(data)
Enter fullscreen mode Exit fullscreen mode

In the above example, function_n is an imaginary pure function.
The fact that the function is called 5 times in the pipeline means that data is copied 5 times.
This is because object copying is a very expensive operation, which is why JavaScript, Rust, and other modern programming languages use references for object allocation.
So, how do we solve this problem?
In fact, we don't need to worry about it.
All we need to do is add deep_copy to the beginning of the pipeline.

. . . . . .
const data = { ...somethings }
const new_data = pipe(
  deep_copy,
  function_1,
  function_2,
  function_3,
  function_4,
  function_5
)(data)
Enter fullscreen mode Exit fullscreen mode

However, this solution requires function_n to be mutable, which is a violation of the functional programming principle of immutability.

Implementing Counter

Next, let's implement the Counter feature using FP(Functional programming, OOP(Object-oriented programming), and ECS(Entity component system) methods.

FP

function increment(num) {
  return num + 1
}
const counter = {
  _count: 0,
  get_count() {
    return this._count
  },
  set_count(count) {
    this._count = count
  }
}
button.onclick = pipe(
  counter.get_count,
  increment,
  counter.set_count
)
Enter fullscreen mode Exit fullscreen mode

To avoid side effects, we separate the increment function to an external pure function, and count is changed by getters and setters.
In fact, even the above code is not entirely FP-compliant for a few reasons.

OOP

const counter = {
  _count: 0,
  get_count() {
    return this._count
  },
  increment() {
    this._count++
  }
}
button.onclick = counter.increment
Enter fullscreen mode Exit fullscreen mode


js
In fact, the above code is not entirely OOP-compliant.
However, the idea we can get from the above code is that we can improve the maintainability of the code by limiting the side effects of the functions in the object to the internal of the object.

ECS

const counter = { count: 0 }
/** @type {Record<string, (entity: { count: number }) => *>} */
const CounterSystem = {
  increment(entity) {
    entity.count++
  }
}
button.onclick = () => CounterSystem.increment(counter)
Enter fullscreen mode Exit fullscreen mode

ECS is a way of separating data and functionality, simply put.
This is a very interesting approach, but here I will only mention it as a simple comparison.
What is the development experience of each of the three methods when used?
FP is definitely slower than the other two methods.
However, FP also has a definite advantage, which is its excellent maintainability through declarative programming.
In addition, there are no constraints when performing asynchronous parallel tasks because there are no side effects.
However, this is only the claim of the FP camp, and if you look at the sample code above, FP does not seem to be more intuitive than the other two methods, nor does it seem to be easier to maintain.
Therefore, the question we need to ask here is whether declarative programming is really more maintainable than procedural programming?
And is thread safety in parallel processing the exclusive domain of functional programming?

Fast inverse square root

Fast InvSqrt is a function that calculates the inverse square root very quickly by exploiting the way that floating-point numbers are represented in memory.
It is a very famous algorithm that was used in the game Quake III Arena, which was released in 1999.

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y = number;
    i = * ( long * ) &y; // evil floating point bit level hacking
    i = 0x5f3759df - ( i >> 1 ); // what the fuck?
    y = * ( float * ) &i;
    y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
//  y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed

    return y;
}
Enter fullscreen mode Exit fullscreen mode

This code isn't long, but I don't understand how it works.
But that's okay.
I just need to know that Q_rsqrt(number) returns an approximation of 1 / sqrt(number).
It is also clear that Q_rsqrt needs to be modified to support the faster and more accurate built-in CPU operation RSQRTSS.
In programming, functions are typically units of functionality or modification.
The name of a function serves as a roadmap, summarizing the logic within the function and allowing for an understanding of the overall flow.
Personally, I believe that investing the skill and effort required to properly implement functional programming in coding conventions and test code is the way to write higher-quality programs.

The pitfalls of immutability

Immutability prevents side effects and keeps program structure simple.
Is this really true?
Let's compare the code of React and Svelte in a simple way

export function App() {
  const [count, set_count] = useState([0, 2])
  function handle_click() {
    count[0] += count[1]
    set_count(count)
  }
  return (
    <button onClick={handle_click}>
      Clicked {count[0]}
      {count[0] <= 1 ? " time" : " times"}
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode
<script>
  let count = [ 0, 2 ]

  function handle_click() {
    count[0] += count[1]
  }
</script>

<button on:click={handle_click}>
  Clicked {count[0]}
  {count[0] <= 1 ? "time" : "times"}
</button>
Enter fullscreen mode Exit fullscreen mode

Yes, I have implemented a simple counter function with React and Svelte.
However, in fact, the React code in the above example is not working properly.
To make the above code work properly, we need to modify the handle_click function as follows.

  function handle_click() {
    count[0] += count[1]
    // set_count(count)
    set_count([...count])
  }
Enter fullscreen mode Exit fullscreen mode

The reason is that React needs to provide a new object to notify the state change.
React is a framework that accounts for 82% of the front-end usage as of State of JS 2022.
React's constraint of immutability is being transformed into a condition for clean code due to its high market share.
Today's front-end frameworks allow us to write components declaratively.
Even if we use the same data on multiple screens, rendering does not modify the data, so it is not a problem. And if the data is changed, it means that the view needs to be changed.
However, in React, we need to copy objects every time the state changes, which is simply pure overhead.

Conclusion

Limiting side effects and streamlining the flow of code are essential elements for improving developer experience, such as program maintenance.
However, I personally believe that functional programming is a misguided approach from the perspective of developer experience, as it not only significantly impairs code performance, but also the flow of code.
Note that the criticism in this article is limited to extreme functional programming.
JavaScript is a multi-paradigm programming language that is flexible enough to incorporate the benefits of multiple paradigms.
Thank you.

Top comments (14)

Collapse
 
lionelrowe profile image
lionel-rowe

Your "FP" example:

function increment(num) {
  return num + 1
}
const counter = {
  _count: 0,
  get_count() {
    return this._count
  },
  set_count(count) {
    this._count = count
  }
}
button.onclick = pipe(
  counter.get_count,
  increment,
  counter.set_count
)
Enter fullscreen mode Exit fullscreen mode

...isn't remotely close to FP. Passing impure methods of a mutable object to a higher-order function named pipe doesn't magically make them pure. The only pure function here is increment.

Collapse
 
artxe2 profile image
Yeom suyun

Yes, that is a correct observation.
Unfortunately, the use of the this keyword in the getter and setter in the example is not pure in any way.
I think it is an exception that the beginning and end of the pipeline violate FP even when using functional programming libraries such as RX.js.
The example description states that the example does not meet FP for several reasons, but the related content is definitely poor.

Collapse
 
skyjur profile image
Ski • Edited
  1. Why do you say that it needs to copy data 5 times (or n times for n number of functions in pipeline)? When working with larger objects I would expect a tree/like structure where functions apply modifications with minimal data being copied.

  2. Why in your counter example you're using array, and why count[0] += count[1], and why it's initialized with [0, 2] ? Seems like every case of count click will increment by 2. But it's just such an akward way to write this code. Why array in first place and not just one integer value?

  3. Regarding react example my personal opinion is just that we all should simply agree that React hooks in fact is not functional programming. It's actually a lot more like oop than it is functional programming: as it allows declaring and managing encapsulated state inside your components - much same way how we typically do in object instances in oop - think of component as instance and think of useState() as private variables. In fact such components used to be written in class-based manner but React team for whatever reason decided to confuse the hell out of developers and discard 'class' syntax in favor of 'function' syntax even though it's still OOP. Don't get tricked by syntax that suggest that it's "functional" component. If it was functional component it would not have effects and state as functional code cannot have state and effects if they do it's not functional code anymore.

Collapse
 
artxe2 profile image
Yeom suyun
  1. Yes, your opinion is entirely correct.
    In my article, I provided an example of minimizing the copy of values by using deep_copy instead of pure functions.
    The reason why the data needs to be copied 5 times is because pure functions in a pure function pipeline cannot modify the input.

  2. This is artificial code to demonstrate how React enforces immutability.

  3. I'm not sure if React's functional components are closer to OOP, but I completely agree that they are not FP.
    Functional components explicitly cause side effects, which is a very strange way to do things.
    As I said in number 2, the React example was used to argue that immutability is not a condition for clean code.

Collapse
 
skyjur profile image
Ski • Edited

Just to expand a little more on #1. I guess you're probably saying same thing but I think something still needs to be mentioned here on case of copying and copy performance.

I don't think the 'deep_copy' that you added is at all necessary.

My point is basically that, even though input is immutable, function can return a reference to original input without full copy.

For instance we have object

{
      a: {...},
      b: {...},
      c: {...}
}
Enter fullscreen mode Exit fullscreen mode

let's say we want to modify a with new attribute obj.a.name = 'new name', we can do that without copying most of data

{
    a: {...input.a, name: 'new name'},
    b: input.b,
    c: input.c
}
Enter fullscreen mode Exit fullscreen mode

We do have some copying happening, the root object is changed, and the 'a' object is copied, but content of 'b' and 'c' doesn't need to be copied, neither any inner values of a.

So it can be argued that in many cases copying can be done in lightweight shallow manner it wouldn't affect performance.

That said in many more demanding situations this still cannot achieve same performance characteristics as mutable structures. Sometimes mutable structures will simply have significant performance benefit. One reason is just the fact that when we're doing many shallow copies, objects get fragmented in memory. CPUs nowdays are designed to fetch memory in blocks not one byte by one byte and thus if we have high fragmentation we lose this benefit as well as benefit of CPU cache. But yet another reason in garbage collected languages like javascript - this approach is producing more garbage and thus is making garbage collector work more heavily which then impacts performance.

If we could safely update objects in place whilst still having benefits of functional programming that would be ideal and I think it's possible - but not many languages are supporting such concept. This could be relatively easily done by a keyword that would remove

function increment(consumable x) {
    x.count += 1
    return x
} 


function useIncrement() {
    let x = {count: 0}

    let newX = increment(x) //ok 

    x.count // error, x cannot be referenced after it was "consumed" by "increment"

    newX.count // ok
}
Enter fullscreen mode Exit fullscreen mode

such syntax would keep all functional benefits whilst still enabling working with mutable structures. But of course it is quite challenging to implement such concept as very big difficulties begin with references to deep values.

Thread Thread
 
artxe2 profile image
Yeom suyun • Edited

Sure, I agree with you.
In many cases, pure functions may not need deep copies, and deep_copy can be optionally used.
(The example using deep_copy assumes that the pure function constraint does not apply)

The last example looks very similar to the concept in Rust. It would be interesting if this approach was supported at the IDE level, such as JSDoc or ESLint.

Collapse
 
aregtech profile image
Artak Avetyan

Clean code has nothing to do with performance. Performance is optimization. Clean code is about readability and usability. If big projects don't follow clean code, they endup with chaos, where each new feature and extension is a pain.

Collapse
 
artxe2 profile image
Yeom suyun

My post may have been poorly written and failed to convey my meaning. I did not intend to criticize Clean Code.

My point is that it is more aligned with human thinking to limit the scope of side effects to a certain scope, such as objects or files, rather than trying to completely eliminate side effects. Therefore, FP's pure functions are not only slower, as everyone acknowledges, but also do not seem to have any maintenance advantages over OOP or others.

Collapse
 
aregtech profile image
Artak Avetyan

My comment was mainly referred to the article, because the author was criticizing the clean code and bringing examples to prove that there are codes with better performance.

About the FP, it has advantages. One of them, with FP you make easier modular software. If the concept of OOP is around object, data and its states, in FP the concept of states and orders is discarded. Just recently I wrote a small text about OOP and FP essentials. I think these 2 concepts are not competing, but extend each other.

As a practical example, I'm working on an open source framework, which is based on ORPC (Object RPC). On one hand the RPC can be considered as FP, on the other hand, it is object based, meaning it may have Data (Attributes) and Types. In short, the framework eases multithreading and multiprocessing programming by treating remote objects like locals, and it makes the location of objects transparent. Once you define the interface of your object (Service Interface), you can make multiple implementations of the same interface (FP concept) and even instantiate same implemented object multiple times, but you access them by their unique names (OOP concept) called Role Names. Meaning the consumer of the remote object should know both -- the Service Interface and the Role Name. In my opinion, here the FP and OOP concepts are quite well intertwined.

Thread Thread
 
artxe2 profile image
Yeom suyun • Edited

I have read the article and GitHub README.md you provided, but I apologize that I did not understand it properly due to my limited knowledge.
It seems that you have introduced the concepts of FP to the advantages of OOP, such as forcing the implementation of certain functions according to objects or reusing functions such as lifecycle, to isolate from the environment and prevent side effects.
I think the concepts of FP and OOP or other methodologies can certainly be integrated.
In the end of my article, I also limited the target of criticism to extreme functional programming.
However, in extreme functional programming, functions cannot read any data other than the input, and cannot write any data other than the output.
Everything must be implemented declaratively by combining these pure functions.
And I think this is probably an unnecessary constraint for you as well.

Thread Thread
 
aregtech profile image
Artak Avetyan

Yes, agree. That's why I think it is suitable for modular applications, where you are not interested what is inside modules, you are focusing on input and outputs of the modules. Also with FP it is easier to scale the modules. This is where I see the biggest advantage of the FP, rather than in performance.

Collapse
 
booniepepper profile image
J.R. Hill

What did I even just read? Bad code is bad regardless of whether it's OO or FP or something else.

Collapse
 
artxe2 profile image
Yeom suyun

Maybe this is the key.
The condition of good code is irrelevant to whether it is written in FP.

Collapse
 
bwca profile image
Volodymyr Yepishev

The zealots will be upon you in no time 😂