Reduce method is the mother of several other methods

I have read it in different sections of Functional Programming that

"`map` and `filter` are special cases of `reduce`  or 
these methods can be explained in terms of reduce method. 

I didn’t understand it.
Can you please elaborate it using some examples for me to understand it better? Thanks a lot in advance!

Hello there,

I would not really agree with that sentence, as is. I would put it like this:

Anything map and filter can do, reduce can do as well.

It might event be correct to say (I cannot think of an example disproving):

Anything reduce can do, map can do as well.

This does not mean you should always use one or another. Just explains how these methods are similar in procedure.

Examples might be:

const myArr = [1, 2, 3, 4];
// Adding with reduce
const usingReduce = myArr.reduce((acc, curr) => acc + curr);
// Adding with map
let total = 0
const usingMap = myArr.map((ele) => total+=ele);

console.log(usingReduce)
console.log(usingMap[usingMap.length-1])

You will notice they are quite different, and that is why using map to add is usually not the way to go.


If you are looking for more examples of these methods, then I recommend you look at the docs:
Array.prototype.map() - JavaScript | MDN (mozilla.org)

Hope this helps

2 Likes

Any of the other array methods can be implemented using reduce. They will often be a lot more inefficient because the actual methods normally use quite optimised loops but for example:

// map (multiply each array element by 2)
[1,2,3,4,5].reduce((acc, v) => [...acc, v * 2], []);

// filter (filter out odd numbers)
[1,2,3,4,5].reduce((acc, v) => v % 2 === 0 ? [...acc, v] : acc, []);

// lastIndexOf (find index of value "5")
[1,2,3,4,5].reduce((acc, v, i) => v === 5 ? i : acc, -1);

// reverse
[1,2,3,4,5].reduce((acc, v) => [v, ...acc], []);

// forEach (log the values)
[1,2,3,4,5].reduce((acc, v) => console.log(v), undefined);

// every (check if every value is less than 6)
[1,2,3,4,5].reduce((acc, v) => v < 6, false);

// fill (with 0 from position 2 to position 4)
[1,2,3,4,5].reduce((acc, v, i) => [...acc,  i >= 2 && i <= 4 ? 0 : v], []);

// join (with space)
[1,2,3,4,5].reduce((acc, v, i) => i === 0 ? `${v}` : `${acc} ${v}`, "");

// slice (from index 1 to 3)
[1,2,3,4,5].reduce((acc, v, i) => i >= 1 && i <= 3 ? [...acc, v] : acc, []);

Note

  1. I haven’t tested any of these and there are probably a few mistakes, I’ve done it very quickly.
  2. I’ve just written it the way I have for brevity, not one of those is at all optimised.
  3. A few I’ve ignored, like find and includes, because they need to either stop the iteration at a certain point (which is possible but involves throwing an error) or need to be wrapped in another function and it’s a pita to implement in a few minutes
  4. Couldn’t be bothered to implement flat (or, from that, flatMap), again, for time

I can fix above issues and write them out in slightly less compact fashion if you want a bit more explanation.

4 Likes

I’m not sure this would work? you are getting only the value from last v<6
I would expect instead having a return statement of acc && v<6 and starfing value of true

other than that, that was a lot of work!

1 Like

Ah yeah, I missed that, you’re right. That’s one of the ones that needs to either stop iterating, or be wrapped in another function, or do some other wierdness

1 Like

map can only take an array and return an array of the same size. Side effects don’t count: the array HOFs are assumed to be pure, forEach notwithstanding. reduce can return anything, and can in fact can compute anything at all.

1 Like

this guys playlist helped me out with all the higer order functions a few years back! check it out Higher-order functions - Part 1 of Functional Programming in JavaScript - YouTube

Right here we go. Ones that don’t make sense (eg reduce itself, toString, etc, etc) are commented out. I just realised I forgot flatMap, but I should be doing work anyway, so hey ho. I haven’t tested all of them so there may be a few slight errors.

One thing to note is how I’ve exited the loop where necessary: reduce et al are designed to iterate through an entire array. But the fourth argument to the reduce callback is the actual array currently being iterated over. By mutating it using splice when I want to break out of it, I can change the length (thisArr.splice(1)). Because the length has been changed and there are no more entries, reduce will finish iterating. Note that this will actually mutate the input array, so this is an awful hack that won’t generally be useful in practice, it’s just used as an example here because I didn’t have time to think of anything better.

  • no mutator methods (push, pop, shift, unshift or splice). These are used within the reduce function
  • no reflection-like methods (toSource etc) as they don’t make sense in this context.
  • no sort as I’m not writing a sort implementation
  • no reduce or reduceRight as that defeats the point (however… *)

Just to clarify, don’t actually do any of this :point_down: , it’s all pretty awful (though on the whole I think it’s fairly efficient), use the defined methods

function at(arr, n) {
    if (n < 0) {
        n = arr.length + n;
    }
    return arr.reduce((acc, v, i, thisArr) => {
        if (i === n) {
            thisArr.splice(1);
            return v;
        } else {
            return acc;
        }
    }, undefined);
}

// function concat() {}

function copyWithin(arr, target, start = 0, end = arr.length) {
    let replaceCount = end - start;
    return arr.reduce((acc, v, i) => {
        if (i >= target && replaceCount !== 0) {
            acc[i] = arr[end - replaceCount];
            replaceCount--
        } else {
            acc[i] = v;
        }
        return acc;
    }, []);
}

function entries(arr) {
    return arr.reduce((acc, v, i) => {
        acc.push([i, v]);
        return acc;
    }, []);
}

function every(arr, cb) {
    return arr.reduce((acc, v, _i, thisArr) => {
        if (!cb(v)) {
            thisArr.splice(1);
            return false;
        }
      	return acc;
    }, true);
}

function fill(arr, val, start = 0, end = arr.length) {
    // NOTE slightly incomplete: I haven't handled negative values:
    return arr.reduce((acc, v, i) => {
        if (i >= start && i < end) {
            acc[i] = val;
        } else {
            acc[i] = v;
        }
        return acc;
    }, []);
}

function filter(arr, cb) {
    return arr.reduce((acc, v) => {
        if (cb(v)) {
            acc.push(v);
        }
        return acc;
    }, [])
}


function find(arr, cb) {
    return arr.reduce((acc, v, _i, thisArr) => {
        if (cb(v)) {
            thisArr.splice(1);
            return v;
        }
        return acc;
    }, undefined);
}

function findIndex(arr, cb) {
    return arr.reduce((acc, v, i, thisArr) => {
        if (cb(v)) {
            thisArr.splice(1);
            return i;
        }
        return acc;
    }, -1);
}

function flat(arr, depth = 1) {
    if (depth > 0) {
        return arr.reduce((acc, val) => {
            if (Array.isArray(val)) {
                acc.push(flat(val, depth - 1));
            } else {
                acc.push(val);
            }
            return acc;
        }, [])
     } else {
         return slice(arr);
     }
}

function flatMap(arr, cb) {}

function forEach(arr, cb) {
    return arr.reduce((acc, val) => {
        cb(val);
        return acc;
    }, undefined);
}

function includes(arr, val) {
    return arr.reduce((acc, v, _i, thisArr) => {
        if (v === val) {
            thisArr.splice(1);
            return true;
        } else {
            return acc;
        }
    }, false)
}

function indexOf(arr, val) {
    return arr.reduce((acc, v, i, thisArr) => {
        if (v === val) {
            thisArr.splice(1);
            return i;
        } else {
            return acc;
        }
    }, -1)
}

function join(arr, joiner = ",") {
    return arr.reduce((acc, v, i) => {
        if (i === 0) {
            return `${v}`;
        } else {
            return `${acc}${joiner}${v}`;
        }
    }, "")
}

function keys(arr) {
    return arr.reduce((acc, _v, i) => {
        acc.push(i);
        return acc;
    }, [])
}

function lastIndexOf(arr, val) {
    return arr.reduce((acc, v, i) => {
        if (v === val) {
            return i;
        } else {
            return acc;
        }
    }, -1)
}

function map(arr, cb) {
    return arr.reduce((acc, v) => {
        acc.push(cb(v));
        return acc;
    }, [])
}

// function pop() {}
// function push() {}
// function reduce() {}
// function reduceRight() {}

function reverse(arr) {
    return arr.reduce((acc, v) => {
        acc.unshift(v);
        return acc;
    }, []);
}

// function shift() {}

function slice(arr, start = 0, end = arr.length) {
    if (start < 0) {
    start = end + start;
  }

  return arr.reduce((acc, v, i, thisArr) => {
      if (i >= end) {
          thisArr.splice(1);
      } else if (i >= start) {
          acc.push(v);
      }
      return acc;
  }, [])
}

function some(arr, cb) {
    return arr.reduce((acc, v, _i, thisArr) => {
        if (cb(v)) {
            thisArr.splice(1);
            return true;
        } else {
            return acc;
        }
    }, false)
}

// function sort() {}
// function splice() {}
// function toLocaleString() {}
// function toSource() {}
// function toString() {}
// function unshift() {}

function values(arr) {
    return arr.reduce((acc, v) => {
        acc.push(v);
        return acc
    }, []);
}

* bonus. I think this is robust – anyway all the above can use this instead of the Array.prototype.reduce, although doing that would be an even more terrible idea:

function reduce(arr, cb, acc, i = 0) {
  const [head, ...tail] = arr;

  if (head === undefined) {
    return acc;
  } else {
    return reduce(tail, cb, cb(acc, head, i, arr), i + 1);
  }
}
1 Like