The magic of reduce

TL;DR: When I first saw the `reduce` function in the web dev bootcamp - I was completely overwhelmed. With time and experience, I came to love it. In this piece, I attempt to dismantle the complexity of this array method, show its beauty and versatility.

"Reduce is easy," - our bootcamp instructor said. "All you need to do is to pass the function two arguments. The first argument is the callback, and the second is the accumulator."

He could’ve been speaking Chinese because I barely understood what he was saying. At this point in my JS journey, I was trying to comprehend the difference between function parameters and arguments. I also wasn’t sure what callbacks were. The only thing I knew about callbacks back then was that when someone said "callback hell", I was supposed to laugh.

Long story short, my eyes almost popped when the instructor used the reduce method on an array and got an object in the end. I was so confused, and I decided I’d never do anything with reduce.

One or two years later, I was using reduce all over the place without realizing it. I felt so overconfident as to think that reduce is the method to rule over others. The following code is simply an exercise. Please use the native methods instead of doing custom shenanigans whenever possible.

The reduce method accepts two parameters: the callback and the accumulator. I think it’s pretty hard to understand what reduce does by reading an explanation. But looking at some examples helps to get what’s happening. In all examples, we will go through the following steps:

  1. As the accumulator, provide a value of the same type you want to get out of the function. (.map returns an array, so the accumulator we pass will be an empty array, .join returns a string, so we pass an empty string, .indexOf returns a number, so we pass a -1, etc.)
  2. We provide a function that will run for each item in the array and determine the new accumulated value.

You can find all the following code in my Reduce study repo, including test cases.

Reduce vs. Join

// We fall back to a comma, when no separator is passed, as the native join.
function customJoin(array, separator = ',') {
  return array.reduce(function (acc, val, ind, arr) {
    // The only "tricky" bit is not adding a separator in the last iteration.
    // Handily we get the array and the index of the current element as the
    // argument.
    return (acc += val + (ind === arr.length - 1 ? '' : separator))
  }, '') // The return value is a string
}

Reduce vs. Map

function customMap(array, callback) {
  return array.reduce((acc, ...rest) => {
    // Simply add the result of the callback to the existing array.
    return [...acc, callback(...rest)]
  }, []) // The return value is an array
}

export default customMap

Reduce vs. Filter

function customFilter(array, condition) {
  return array.reduce(function (acc, val, ind, arr) {
    // If the value meets the condition, add the value to the end result
    if (condition(val, ind, arr)) acc.push(val)

    return acc
  }, []) // The return value is an array
}

Reduce vs. Find

function customFind(array, condition) {
  return array.reduce(function (acc, val, ind, arr) {
    // Find returns the first item that fits the condition. So if there's no
    // match yet, set the `acc` to the current item. But if there was already
    // a match, don't do anything
    if (!acc && condition(val, ind, arr)) acc = val

    return acc
  }, undefined) // If no item meets the condition `undefined` is returned
}

Reduce vs. FindIndex

function customFind(array, condition) {
  return array.reduce(function (acc, val, ind, arr) {
    // Find returns the first item that fits the condition. So if there's no
    // match yet, set the `acc` to the current index. But if there was already
    // a match, don't do anything
    if (!acc && condition(val, ind, arr)) acc = index

    return acc
  }, -1) // If no item meets the condition `-1` is returned
}

Reduce vs. Slice

function customFindIndex(array, start, end) {
  // Early return for empty arrays
  if (array.length === 0) return []

  // When no `end` is provided, we fall back to the `array.length`, as that
  // is the highest index there can possibly be. If the array has 5 items, set
  // the `end` to 5.
  // Also handle cases in which the `start` and `end` are negative.
  const sliceEnd = (end < 0 ? array.length + end : end) || array.length
  const sliceStart = start < 0 ? array.length + start : start

  return array.reduce(function (acc, val, ind) {
    // Add current element to end result, if its index is in matching range
    if (ind >= sliceStart && ind < sliceEnd) acc.push(val)

    return acc
  }, []) // Default return value
}

Reduce vs. Fill

function customFill(array, content, start, end) {
  // Make sure the `start` and `end` are defined.
  const fillEnd = end || array.length
  const fillStart = start || 0

  return array.reduce(function (acc, val, ind) {
    if (ind >= fillStart && ind < fillEnd) acc.push(content)
    else acc.push(val)

    return acc
  }, [])
}

Reduce vs. Flat

Writing a custom flat function was great fun. I started my developer career after completing a bootcamp for web development. I was generally doing pretty well. The only thing I never got was recursion. I cannot express my delight that while implementing this custom flat function, it worked from the first try without getting stuck in infinite loops.

// Make sure that the depth is always defined.
function customFlat(array, depth = 1) {
  // Prevent getting stuck in infinite loops
  if (typeof depth !== 'number') {
    console.error('Provide a valid depth: ', depth)
    return []
  }

  // Return early for empty arrays
  if (array.length === 0) return []

  // Always flat the array by one level
  const flatByOne = array.reduce(function (acc, val) {
    // Flat element by spreading, if element is an array
    if (Array.isArray(val)) return [...acc, ...val]
    else acc.push(val)

    return acc
  }, [])

  if (depth === 1) return flatByOne

  return customFlat(flatByOne, depth - 1)
}

Reduce vs. Includes

function customIncludes(array, condition) {
  if (array.length === 0) return false

  return array.reduce(function (acc, val, ind, arr) {
    // If we ever set acc to `true` we should never reassign it.
    if (!acc && val === condition) acc = true
    return acc
  }, false)
}

Reduce vs. Reverse

function customReverse(array) {
  return array.reduce((acc, val) => {
    return [val, ...acc]
  }, [])
}

Reduce vs. Every

function customIncludes(array, condition) {
  if (array.length === 0) return true

  return array.reduce(function (acc, val, ind, arr) {
    if (!condition(val)) acc = false

    return acc
  }, true)
}