Pure Critique of Pure Functions in Functional Programming

artxe2

Yeom suyun

Posted on September 5, 2023

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.

💖 💪 🙅 🚩
artxe2
Yeom suyun

Posted on September 5, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related