loading...

Reducing Readability?

qmaximillian profile image Quinn Lashinsky Originally published at Medium ・4 min read

Originally posted on Medium - 02-21-2020

Recently, I found myself working through a code challenge where I had to fetch some data, and then transform it into an object. Prior to seeing the challenge, I had been working on becoming more familiar with different Javascript’s Array methods and my interest was piqued by the reduce method. Because the challenge wanted me to transform an array into an object I immediately thought to myself, this is the perfect chance for me to use reduce!

Initially, when I first approached reduce I found examples that focused mostly on arithmetic. Adding, subtracting and manipulating numbers to return a desired result. I wanted to take the next step and utilize this powerful function to perform object transforms and write less code that was more efficient.

Simple Reduce

Here is an example of a simple usage of reduce.

const numberArray = [1, 2, 3, 4]
const initialValue = 0

const summedArray = numberArray.reduce((accumulatedValue, currentValue) => {
  return accumulatedValue + currentValue;
}, initialValue)

console.log("summedArray", summedArray)
// summedArray 10

By using reduce we are able to sum all of the numbers in the numberArray

When we invoke the reduce method:

  1. Our accumulatedValue = initialValue.
  2. Our currentValue = 1, the first index in our array.
  3. accumulatedValue + currentValue = 0 + 1 = 1. What we return from our callback function is our new accumulatedValue which is 1.
  4. Next iteration
  5. accumulatedValue = 1
  6. currentValue = 2.
  7. accumulatedValue + currentValue = 1+ 2= 3. What we return from our callback function is our new accumulatedValue which is 3.
  8. This continues until we’ve iterated through the entire array, finally returning our last accumulatedValue which is 10, the sum of all numbers in the numberArray.

More “Advanced” Reduce

In the code challenge I was given an array that I had to transform into an object. I had a couple of requirements. I had to

  • Create new keys within that new object
  • Conditionally add keys to each item

Here is an example of the mock data I was given:

const data = [
    { id: 1, favoriteColor: "brown", disposition: "closed" },
    { id: 2, favoriteColor: "yellow", disposition: "open" },
    { id: 3, favoriteColor: "brown", disposition: "closed" },
    { id: 4, favoriteColor: "brown", disposition: "open" },
    { id: 5, favoriteColor: "red", disposition: "closed" },
    { id: 6, favoriteColor: "blue", disposition: "open" },
    { id: 7, favoriteColor: "green", disposition: "closed" },
    { id: 8, favoriteColor: "green", disposition: "open" },
    { id: 9, favoriteColor: "brown", disposition: "closed" },
    { id: 10, favoriteColor: "red", disposition: "open" }
]

The new object had to:

  1. Create an “ids” key with an empty array as its value and return the id of each item.
  2. Create an “open” key with an empty array as its value and add an item if its disposition value is “open”.
  3. For each item with disposition value of “open”, add a fourth key called “isPrimary” indicating whether the value is a primary color or not.
  4. Create a “closedCount” key with a value of 0. If the items favoriteColor value is a primary color and disposition value is “closed” increment “closedCount” by 1.

First Step

Before I tackled each requirement I knew I would have to create a new object that would include “ids”, “open” and “closedCount” keys. I would define this object and include it as the second parameter to our reduce function, our initialValue.

let reducer = (accumulatedValue, currentValue) = > {
  // function block we haven't defined yet
}

let initialValue = {
    id: [],
    open: [],
    closedCount: 0
}

data.reduce(
    reducer
    initialValue
)

Defining an initial value for our reduce function

Defining an initialValue can prevent us from trying to access a key that doesn’t exist on our accumulated object while defining the shape of our new object.

function isPrimary(color){
    if (color === 'yellow' || color === 'red' || color === 'blue') {
      return true
    }
    return false
}

With this in place, we can define our reducer function. We’ll also be using an “isPrimary” helper function to determine if an item has a primary color.

On each iteration, by checking the currentValue we can decide whether our requirements for our new object are being met and imperatively change our accumulatedValue as necessary. We just need to make sure we return our accumulatedValue at the end of our reducer function.

function reducer(accumulatedValue, currentValue){
// ids
  accumulatedValue.ids.push(currentValue.id)
// open
  if (currentValue.disposition === 'open'){
      accumulatedValue.open.push({
          ...currentValue,
          isPrimary: isPrimary(currentValue.favoriteColor)
      })
  }
// closedCount
  if (currentValue.disposition === 'closed' &&
        isPrimary(currentValue.favoriteColor)) {
          accumulatedValue.closedCount++
        }
  return accumulatedValue 
}

And afterward we end up with our transformed data:

{
  ids: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
  open: [
    {
      ids: 2,
      favoriteColor: 'yellow',
      disposition: 'open',
      isPrimary: true
    },
    {
      ids: 4,
      favoriteColor: 'brown',
      disposition: 'open',
      isPrimary: false
    },
    {
      ids: 6,
      favoriteColor: 'blue',
      disposition: 'open',
      isPrimary: true
    },
    {
      ids: 8,
      favoriteColor: 'green',
      disposition: 'open',
      isPrimary: false
    },
    {
      ids: 10,
      favoriteColor: 'red',
      disposition: 'open',
      isPrimary: true
    },
  ],
  closedCount: 1
}

While this would work, I couldn’t help but think how tightly coupled the logic is within the reduce function. If we were creating a much larger object, this could make it harder to reason about and make our callback function more error prone. While I loved the idea of being able to use reduce to encapsulate all of my logic, I felt that there was a simpler, more flexible way to achieve the same result.

Map and Filter

let formattedObj = {
  ids: data.map(item => item.id),
  open: data.filter(
    item => item.disposition === 'open' &&
    {...item, isPrimary: isPrimary(item.favoriteColor)}
  ),
  closedPrimaryCount: data.filter(
    item => {
      if (item.disposition === 'closed' && 
           isPrimary(item.favoriteColor)) {
             return item
         }
    }).length
}

By returning exactly the data we want for each key, we don’t have to worry about accidentally changing or affecting any of the other keys on our object. Any change we need to make will be directly tied to the key, making our code more declarative and easier to reason about than before.

Posted on by:

qmaximillian profile

Quinn Lashinsky

@qmaximillian

Front End Developer. Huge fan of Nintendo, baseball, cats, and fitness.

Discussion

pic
Editor guide