Mapping and reducing are two commonly used operations in functional programming. While mapping isn’t a concept that always relates to arrays/lists or collections in general; reduce totally relates to them and sometimes you can “reduce/fold” not only a collection but also a Stream (a concept of “values that come over time”) or a Signal; an asynchronous channel or mailbox messages in actor-based or go-loop-based languages such as Go/Elm/Elixir.
Since we’re specifically talking about JavaScript, and .reduce/.map are both only implemented as array methods, I can explain try to explain them in both contexts. Also here’s a quirky image I found on twitter:
Answering your questions directly:
1- The parameter “group” refers to each individual sub-array in the larger array (arr)
2- Yes and no. The map is creating a brand new array, but “group” does not refer to the newly created array, but rather —as explained above— to each individual sub-array that you receive in the callback function to be later transformed into a single number (the maximum value of that sub-array).
3- No, “map” does not “flatten” innermost arrays, map doesn’t know anything about matrices or innermost anything. It just takes an item from the operated array and sends it to a callback function for it to be transformed; it just so happens that each individual item also happens to be an array; mere coincidence.
function largestOfFour(arr) {
return arr.map(function (group) {
// at the first iteration, "group" will be equal to
// [4,5,1,3], then the 2nd will be [13,27,18,26] and so on
return group.reduce(function(prev, current) {
// here, "prev" will refer to the current maximum number
// and "current" to the current number of each sub-array
return current > prev ? current : prev
}
}
if you wanted to get technical, you could use Math.max(prev, current)
instead of the ternary comparison; and if you wanted to get pedant: use Number.NEGATIVE_INFINITY
as your initial reduce value. And, if you wanted to get code-golfey (minimum possible code), use this hack for Math.max: Math.max(...group)
to avoid doing the reduce.
Read the following stuff if you want to learn new things and listen to my rambles:
Explaining: Map | Mapping
The concept of mapping comes from mathematics where it means going from one value to another value (potentially the same). A function (in math) is even described as the mapping of an input value x (or more, in multivariable calculus) to another output value (after evaluating that function with the provided value or values).
Note: They are also called morphisms in category theory
So as you can see, in functional programming mapping is used for a thing called Functor which is just a contextual container with different behaviours. For example, mapping a “box” is just opening the box, retrieving what’s inside, applying a function or “transformation/mapper” to it, and placing the result inside the same (or a new box of similar type/context; it really depends on the implementation) container. In JavaScript, when you map over an array, you’re not changing the original array, you are returning a new one.
An array happens to follow the functor rules, and thus, you can “map” over it, but as it turns out, an array is a container that contains multiple values. Naturally, when mapping an array, all the elements inside it change; which means, the same mapping function is applied to all the values and placed back in the same order.
As to why the JavaScript array implementation of map receives 3 parameters? It’s just because it facilitates the following items:
- Each element of the array (optional)
- The index of the current element (optional)
- The reference to the original array if you need it (optional)
Why are they all optional? Because even if you omit the current item parameter, you will still map each element to whatever your mapping function returns. Now, is it common to omit the first parameter? Not really but it has its uses, I may have forgotten about real use cases but here’s an example:
[1, 2, 3, 4].map(function() { return 0 })
//> [0, 0, 0, 0]
Sometimes you may not need the first parameter and only need the index; since you can’t skip the first parameter in order to receive the second, you need to specify it in the parameter list but you don’t necessarily need to use it, example (a common pattern for ranges of values):
[...Array(50)].map((_item, index) => index)
//> [0,1,2,3,4,5,6,7,8,9]
As you can see, you never get to use the _item parameter, and it’s a convention to use an underscore for unused parameters in functions.
Reducing / Folding
This is harder to explain, it’s such a mindbending concept for beginners but it can be explained as taking a collection or stream of values and producing a single value from them. In arrays, it just means to consume all of the values to produce a result of any type.
The reduce method of the Array prototype in JS is used for many things, even for creating an object or another array. Did you know you can also define map and filter in terms of reduce? A wild world we live in, rite?
Some languages may refer to this as “folding”, from the left or the right. The reduce method in JS is a left-associative folding operation because it goes from left to right (referring to the elements in the array) and the order of the parameters in the reducer (the function you pass to it).
The “reducer” is a function with 2 obligatory parameters:
- The accumulated value: If you provided an initial value, the first value that this parameter takes is that initial value. If not, the first item happens to become the “initial” value and the reducer never gets called because there are no items to reduce over.
- The current element: Refers to the current item in the array being folded or reduced over. In the case of an initial value provided beforehand, the first iteration takes the first item of the array; otherwise, the first value of this parameter depends on if there are 2 or more values in the array.
It also takes two extra parameters (optional): the index of the current element and the original array.
In hardcore FP, the initial value is called “z” and it’s usually always required to prevent crashing the program. This initial value sometimes falls in line with what we call an “identity” in category theory.
An identity is an element in a monoid (another weird concept) that when combined/concatenated/applied to another element of that same semigroup will leave that other element or value unchanged. Examples?
- Numerical Sums: The identity element of the sum semigroup is 0, because 0 added to any other value will not change said value.
[1,2,3,4].reduce((sum, x) => sum + x, 0)
//> 10 (after 0 + 1 + 2 + 3 + 4)
- Numerical products: The identity is 1 because any number times 1 is that same number.
[5, 6].reduce((product, x) => product * x, 1)
//> 30 (after 1 * 5 * 6)
- String concatenation: Identity is, as you guessed, “” empty string because any string combined with an empty string is the same string:
["hello", "cruel", "world"].reduce((result, x) => result + x, "")
//> "hellocruelworld"
// Why use this when there's String.prototype.join, though...
- Array concatenation: Identity is
[]
(empty array) because any list combined with an empty list doesn’t change, and in similar ways, in a recursive cons’ing (appending to the start or end) of a value to a list, you will eventually need to provide a list to stop the recursion, and what better list than an empty one. You can also use it to build a new list from whatever elements you may already have.
- Object association: Identity is
{}
(empty object) because any object merged with an empty one yields no changes at all. Same rules apply as above but as a key-value pair map.
- Maximum value: -Infinity because any number compared to the lowest possible number will always be the maximum of the two.
- Minimum value: +Infinity because any number will always be tiny in comparison to infinity.
- AND (logical product): True because true && true = true and false && true = false.
- OR (logical addition): False because false || false = false and true || false = true.
Notice how AND being “product” and True being its identity is similar to 1 and true being the same thing? Same with OR and addition with a common identity of 0/false.
- Numerical division: 1 (it’s basically multiplying but by an inverse or 1/x fraction)
- Function composition: Identity is the “identity function” which is just a function that returns its only argument:
x => x
.
Division, subtraction and equality do not fully comply with the monoid (lack associativity) rules so they’re not technically monoids.
const identity = x => x
const compose = (f, g) => x => f(g(x))
const rightToLeftComp = (...funcList) =>
funcList.reduce(compose, identity)
const pipe = (...funcList) =>
funcList.reduceRight(compose, identity)
rightToLeftComp(Math.log, Math.sqrt, Math.cos)(1)
//> -0.3078132351930071
// Equivalent to calling:
// Math.log(Math.sqrt(Math.cos(1)))
pipe(Math.log, Math.sqrt, Math.cos)(1)
//> 1
// Equivalent to calling:
// Math.cos(Math.sqrt(Math.log(1)))
Among many many other cases… (notice the use of reduceRight, which folds from the right up to the leftmost element).
The object and array composition/assoc identities can be used in javascript but in a different way: to build an object or an array from an existing array. For example, a common use case for object building with reduce is to count the number of times each element of an array appears in it (frequency of occurrences). You can accomplish this:
const assoc = (obj, key, value) => {
obj[key] = value
return obj
}
const frequencies = list => list.reduce(function(freqs, item) {
const itemCount = freqs[item]
return itemCount // goes the 2nd route on first iteration
? assoc(freqs, item, itemCount + 1)
: assoc(freqs, item, 1)
}, {})
frequencies([1,1,1,1,1,2,3,3,3,3,1,1])
//> {"1": 7, "2": 1, "3": 4}