comparison of declarative loop and imperative loop

In my journey to transition from an imperative programming style to a declarative one, I've encountered a challenge related to performance when dealing with loops. Specifically, I have a set of original DATA that I need to manipulate in order to achieve 3 desired outcomes: itemsHash, namesHash, rangeItemsHash

// initial data

const DATA = [
  {id: 1, name: 'Alan', date: '2021-01-01', age: 0},
  {id: 2, name: 'Ben', date: '1980-02-02', age: 41},
  {id: 3, name: 'Clara', date: '1959-03-03', age: 61},
]

...

// desired outcome

// itemsHash => {
//   1: {id: 1, name: 'Alan', date: '2021-01-01', age: 0},
//   2: {id: 2, name: 'Ben', date: '1980-02-02', age: 41},
//   3: {id: 3, name: 'Clara', date: '1959-03-03', age: 61},
// }

// namesHash => {1: 'Alan', 2: 'Ben', 3: 'Clara'}

// rangeItemsHash => {
//   minor: [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}],
//   junior: [{id: 2, name: 'Ben', date: '1980-02-02', age: 41}],
//   senior: [{id: 3, name: 'Clara', date: '1959-03-03', age: 61}],
// }
// imperative approach

const itemsHash = {}
const namesHash = {}
const rangeItemsHash = {}

DATA.forEach(person => {
  itemsHash[person.id] = person;
  namesHash[person.id] = person.name;
  if (person.age > 60){
    if (typeof rangeItemsHash['senior'] === 'undefined'){
      rangeItemsHash['senior'] = []
    }
    rangeItemsHash['senior'].push(person)
  }
  else if (person.age > 21){
    if (typeof rangeItemsHash['junior'] === 'undefined'){
      rangeItemsHash['junior'] = []
    }
    rangeItemsHash['junior'].push(person)
  }
  else {
    if (typeof rangeItemsHash['minor'] === 'undefined'){
      rangeItemsHash['minor'] = []
    }
    rangeItemsHash['minor'].push(person)
  }
})
// declarative approach

const itemsHash = R.indexBy(R.prop('id'))(DATA);
const namesHash = R.compose(R.map(R.prop('name')),R.indexBy(R.prop('id')))(DATA);

const gt21 = R.gt(R.__, 21);
const lt60 = R.lte(R.__, 60);
const isMinor = R.lt(R.__, 21);
const isJunior = R.both(gt21, lt60);
const isSenior = R.gt(R.__, 60);


const groups = {minor: isMinor, junior: isJunior, senior: isSenior };

const rangeItemsHash = R.map((method => R.filter(R.compose(method, R.prop('age')))(DATA)))(groups)

In order to achieve the desired outcome, the imperative approach only requires looping through the data once, whereas the declarative approach involves at least 3 loops (itemsHash, namesHash , rangeItemsHash ) Which method is more efficient? Are there any performance trade-offs to consider?

Answer №1

I have a few thoughts on this topic.

First and foremost, it's essential to determine if performance is truly an issue. It's common for developers to optimize code prematurely without identifying the actual bottlenecks in their application. My approach is to prioritize writing clear and simple code initially, with performance considerations in mind but not as the primary focus. If performance becomes a concern, I recommend benchmarking the application to pinpoint the areas that need optimization. In my experience, the optimizations needed are rarely as straightforward as optimizing a loop from three iterations to one.

If optimization within a single loop is necessary, leveraging a reduce call can be effective. Here's an example:

// A snippet demonstrating the use of reduce
const ageGroup = ({age}) => age > 60 ? 'senior' : age > 21 ? 'junior' : 'minor'

const convert = (people) =>
  people.reduce(({itemsHash, namesHash , rangeItemsHash}, person, _, __, group = ageGroup(person)) => ({
    itemsHash: {...itemsHash, [person.id]: person},
    namesHash: {...namesHash, [person.id]: person.name},
    rangeItemsHash: {...rangeItemsHash, [group]: [...(rangeItemsHash[group] || []), person]}
  }), {itemsHash: {}, namesHash: {}, rangeItemsHash: {}})

// Sample data
const data = [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}]

// Demo
console.log(JSON.stringify(
  convert(data)
, null, 4))
(prevent JSON.stringify call to display shared references across output hashes)

From here, there are two potential strategies to enhance this code further.

The first option is to incorporate Ramda, which offers functions to streamline certain aspects of the code. By using R.reduce and functions like evolve, assoc, and over, we can achieve a more declarative style of coding. Here's a modified version:

// Using Ramda for optimization
const ageGroup = ({age}) => age > 60 ? 'senior' : age > 21 ? 'junior' : 'minor'

const convert = (people) =>
  R.reduce(
    (acc, person, group = ageGroup(person)) => evolve({
      itemsHash: assoc(person.id, person),
      namesHash: assoc(person.id, person.name),
      rangeItemsHash: over(lensProp(group), append(person))
    })(acc), {itemsHash: {}, namesHash: {}, rangeItemsHash: {minor: [], junior: [], senior: []}}, 
    people
  )

// Sample data
const data = [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}]

// Demo
console.log(JSON.stringify(
  convert(data)
, null, 4))
(include scripts from Ramda library for above functionality)

Another angle to consider is addressing the performance issue known as "the reduce ({...spread}) anti-pattern." This involves modifying the accumulator object within the reduce callback function. While Ramda may not directly assist with this problem due to its functional nature, custom helper functions can offer a solution while improving code readability. Here's an alternative implementation:

// Enhancing performance via custom utility functions
const push = (x, xs) => ((xs.push(x)), x)
const put = (k, v, o) => ((o[k] = v), o)
const appendTo = (k, v, o) => put(k, push(v, o[k] || []), o)

const ageGroup = ({age}) => age > 60 ? 'senior' : age > 21 ? 'junior' : 'minor'

const convert = (people) =>
  people.reduce(({itemsHash, namesHash, rangeItemsHash}, person, _, __, group = ageGroup(person)) => ({
    itemsHash: put(person.id, person, itemsHash),
    namesHash: put(person.id, person.name, namesHash),
    rangeItemsHash: appendTo(group, person, rangeItemsHash)
  }), {itemsHash: {}, namesHash: {}, rangeItemsHash: {}})

// Sample data
const data = [{id: 1, name: 'Alan', date: '2021-01-01', age: 0}, {id: 2, name: 'Ben', date: '1980-02-02', age: 41}, {id: 3, name: 'Clara', date: '1959-03-03', age: 61}]

// Demo
console.log(JSON.stringify(
  convert(data)
, null, 4))

In conclusion, unless performance issues are clearly identified, extensive optimizations may not be necessary. Prioritizing clean and understandable code is crucial. Utilizing tools like Ramda can simplify complex operations and facilitate code maintenance without sacrificing clarity.

Answer №2

Just like how you can compose functions with

.map(f).map(g) == .map(compose(g, f))
, you can also compose reducers to ensure all results are obtained in a single pass.

The decision to loop once or multiple times when writing declarative code is not necessarily related to the simplicity of the code.

// Implementing reducer logic for all 3 values of interest
// ID: person
const idIndexReducer = (idIndex, p) => 
  ({ ...idIndex, [p.id]: p });

// ID: name
const idNameIndexReducer = (idNameIndex, p) => 
  ({ ...idNameIndex, [p.id]: p.name });
  
// Age
const ageLabel = ({ age }) => age > 60 ? "senior" : age > 40 ? "medior" : "junior";
const ageGroupReducer = (ageGroups, p) => {
  const ageKey = ageLabel(p);
  
  return {
    ...ageGroups,
    [ageKey]: (ageGroups[ageKey] || []).concat(p)
  }
}

// Combining the reducers
const seed = { idIndex: {}, idNameIndex: {}, ageGroups: {} };
const reducer = ({ idIndex, idNameIndex, ageGroups }, p) => ({
  idIndex: idIndexReducer(idIndex, p),
  idNameIndex: idNameIndexReducer(idNameIndex, p),
  ageGroups: ageGroupReducer(ageGroups, p)
})

const DATA = [
  {id: 1, name: 'Alan', date: '2021-01-01', age: 0},
  {id: 2, name: 'Ben', date: '1980-02-02', age: 41},
  {id: 3, name: 'Clara', date: '1959-03-03', age: 61},
]

// Single loop processing
console.log(
  JSON.stringify(DATA.reduce(reducer, seed), null, 2)
);

Regarding the subjectivity of whether it's worthwhile, simplicity and performance play key roles. In my experience, transitioning from 1 to 3 loops for small datasets often yields negligible impact.

Therefore, if leveraging Ramda, I would adhere to:

const { prop, indexBy, map, groupBy, pipe } = R;

const DATA = [
  {id: 1, name: 'Alan', date: '2021-01-01', age: 0},
  {id: 2, name: 'Ben', date: '1980-02-02', age: 41},
  {id: 3, name: 'Clara', date: '1959-03-03', age: 61},
];

const byId = indexBy(prop("id"), DATA);
const nameById = map(prop("name"), byId);
const ageGroups = groupBy(
  pipe(
    prop("age"), 
    age => age > 60 ? "senior" : age > 40 ? "medior" : "junior"
  ),
  DATA
);

console.log(JSON.stringify({ byId, nameById, ageGroups }, null, 2))
<script src="https://cdn.jsdelivr.net/npm/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="6e1c0f030a0f2e5e405c59405f">[email protected]</a>/dist/ramda.min.js"></script>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

ES6: Using DataURI to provide input results in undefined output

In my current project, I am facing a challenge. I am attempting to pass image dataURI as an input from a url link to the image. To achieve this task, I know that I have to utilize canvas and convert it from there. However, since this process involves an &a ...

Exploring the filter method in arrays to selectively print specific values of an object

const array = [ { value: "Value one", label: "Value at one" }, { value: "Value 2", label: "Value at 2" }, { value: "" , label: "Value at 3" } ...

When defining a class property in TypeScript, you can make it optional by not providing

Is there a way to make a property on a Class optional without it being undefined? In the following example, note that the Class constructor takes a type of itself (this is intentional) class Test { foo: number; bar: string; baz?: string; construc ...

What is the best way to locate the final text node within the <p> element?

Looking at the code below, my goal is to identify and retrieve the text node that serves as the last child element. <p class="western" style="margin-bottom: 0.14in"> <font color="#000000"> <font face="Arial, serif"> <font ...

What is the best way to deactivate the time-based trigger in an old version of a Google sheet, while ensuring it remains active in the duplicated new version?

When I initially copied a Google Sheet, I assumed that the app scripts would be duplicated as well. However, it turns out that this is not the case. Here's the background story: I made a version 2 by copying version 1. Because I wanted to ensure that ...

An array filled with unique and non-repeating elements

I want to display random country flags next to each other, making sure they do not match. However, my specific case requires a unique solution for dealing with arrays: function displayRandomFlags() { var flagurls = ["ZPlo8tpmp/chi","cJBo8tpk6/sov","QyLo ...

What could be causing the script to not function properly on 3D text in Unity5?

Here is the code snippet I have been working on: function OnMouseEnter() { GetComponent(Renderer).material.color = Color.grey; } function OnMouseExit() { GetComponent(Renderer).material.color = Color.white; } I've noticed that when I apply t ...

Ways to obtain every image placed onto an element

Using the img tag within div.image-block sets a background. Images can be added to .block3 through drag and drop. Is there a way to create a container that includes all elements from .image-block? <style> .image-block { position: relat ...

Accessing the Div id stored in a session parameter

Is there a way to store the id (div id) into a session variable? This is an example of my code below: <div class='fieldRow'>Options </div> <div id="abcde"></div> <div class='Row'> <?php $user_typ ...

Utilizing one JavaScript file and consistent classes for multiple modals

Is it possible to have multiple modals using the same JS file, classes, and IDs? I created this code: <button id='myBtn'>Order/Discount</button> <div id='myModal' class='modal'> <div clas ...

Javascript: What is the best way to narrow down search results using multiple values, regardless of the sequence in which they are entered?

React: How can I improve search result filtering to accommodate multiple values (irrespective of word order)? Example: I want to search for the title of a document using variations like "phone repair process," "repair phone process," or "process phone repa ...

UI-grid: Triggering a modal window from the filter header template

Is there a way to create a filter that functions as a simple modal window triggered by a click event, but can be displayed on top of a grid when placed within the filterHeaderTemplate? I have encountered an issue where the modal window I created is being ...

When attempting to validate dates with the after: rule in vee-validate while also utilizing a computed field in Vue.js, the validation process may encounter unexpected issues

Below is a codepen with a simple input field for entering dates: <input type="text" v-model="startDate" name="StartDate" v-validate="{ required: false, date_format: 'dd/MM/yyyy', before: maxStartDate }"/> Despite e ...

A guide on executing a double click action on an element in Selenium Webdriver with JavaScript specifically for Safari users

Having trouble double clicking an element in Safari using Java / Webdriver 2.48. All tests work fine on IE, Chrome, and Firefox but Safari does not support Actions. Currently attempting: executor.executeScript("arguments[0].dblclick();", element); or ...

Employ the find() function to ascertain the result of a condition

I'm struggling to grasp how to utilize the find() method in a conditional statement. In simple terms, I want the conditional to evaluate as true if the value of item.label is equal to the value of e.target.value. if (this.props.Items.find(item => ...

Exploring navigation options in VueJS with the router

I recently completed a tutorial on integrating Okta OAuth with VueJS. In my application, I have set up a default page that displays the "App.vue" component and switches to the "About.vue" component upon clicking the "/about" route. However, I encountered a ...

Discover the magic of Bootstrap 3.0 Popovers and Tooltips

I'm struggling with implementing the popover and tooltip features in Bootstrap. While I have successfully implemented drop downs and modals, the tooltips are not styled or positioned correctly as shown in the Bootstrap examples, and the popover featur ...

Obtaining the image with PHP

As a newcomer to PHP and Ajax queries, I am trying to utilize the croppie.js plugin to upload a profile image. However, when I upload the image, it is saved as 1580192100.png. The problem arises when attempting to later retrieve the image based on the user ...

IE8 encountering issues with Jquery ajax() request

My current issue involves submitting data using ajax from forms with a class of ajax. This functionality works flawlessly in Firefox, Safari, and Chrome, but is unsuccessful in Internet Explorer. ajax: function() { $('form.ajax').live(&apo ...

Replace all existing content on the webpage with an exciting Unity game upon clicking

In an interesting experiment, I am trying to hide a secret href that, once clicked, has the power to wipe out everything on the page, leaving it temporarily blank before replacing it with a captivating Unity game that takes over the entire page. Let me sh ...