Need help understanding map

Tell us what’s happening:
I originally thought the map method required both a key and a value but in this particular solution, I only see a single function following map: function (group) {
what exactly does “group” refer to? Is this function creating a brand new array and “group” refers to the newly created array? And does map always break everything down to each individual item in the innermost arrays?

Your code so far


function largestOfFour(arr) {
return arr.map(function (group) {
  return group.reduce(function(prev, current) {
    return current>prev ? current: prev;
  });
});// You can do this!
return arr;
}

largestOfFour([[4, 5, 1, 3], [13, 27, 18, 26], [32, 35, 37, 39], [1000, 1001, 857, 1]]);

Your browser information:

User Agent is: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36.

Challenge: Return Largest Numbers in Arrays

Link to the challenge:
https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/basic-algorithm-scripting/return-largest-numbers-in-arrays

1 Like

The map callback function can take 3 arguments. You can name the parameters of the callback function anything you want, but the important think to understand is specific argument position determines what the argument value is. The first argument passed to the function is the element being iterated over. The second argument is the index of the element being iterated over. Finally, the third argument is the original array in which the map method was called.

In this example code group represents each sub array of the single array being iterated over. The map method always returns a new array.

Not exactly sure what you mean by “break everything down” here. The map method allows you to access each element in the array and return a new array with “modified” elements.

So, not limited to this example specifically, will the map function whenever it’s used anywhere always return a new array? Does the function (group) look at the subarray instead of the single outermost array here because the function is passed through arr.map? Because “subarray” isn’t specifically mentioned, I’m not entirely clear as to what tells the function to look at the subarrays specifically.

I wasn’t sure if you would ever use map to move and work with whole arrays such that when you have an array within an array within an array, you merely map the outermost array to another value or if the map method will always look for the individual element without regard to how many arrays deep it had to search to get to it.

The map method always returns a new array with the same number of elements as the original array. The only thing that may change is the values of the elements.

Yes, because the subarrays are the elements of the outermost array and the elements of the outermost array (arr in this example) are the ones map is iterating over.

It’s a loop, group is just the name of the parameter the function that gets ran has, it could be called anything

function map(inputArr, mappingFunction) {
  let outputArr = [];

  for (let i = 0; i < inputArr.length; i++) {
    outputArr.push(mappingFunction(inputArr[i]));
  }

  return outputArr;
}

Then like

function doSomething(input) {
  return input;
}

map(someArray, doSomething);

.map() first creates an empty array [].

Then it goes through the array you called it on, arr, item by item. However, since you’re only using a single .map(), it’s not looking at your nested array. It’s only concerned with the outermost array, which has 4 groups of numbers, each nested in its own array.

In the map function, arr.map(function (group) { do this to each item in the outermost array }, .map() is calling .reduce() on each of the four groups.

Then, .reduce() is doing its thing, finding and returning only the largest number of each group. After that, .map() tosses whatever it gets back into the array it created.

So, in the end, .map() is going to return the new array it created first, still with 4 items. .map() doesn’t care what those items are, so long as there are 4 of them. .reduce() is the one determining what those items are in this situation: arrays with only a single number.

If you wanted to use .map() on the nested arrays, you would have to call it on the groups inside the outer .map() call:

arr.map(function (group) {
    return group.map(function (item) {
        return { whatever you want to do to the inner arrays }
    }
}

P.S. +1 for Hollow Knight!

1 Like

okay, so for this first line:

return arr.map(function (group) {

tells the function that we’re mapping whatever is inside this array. Could be six items, could be ten. But for our purposes, there are four items in this array. So .map will also expect to have 4 items, which we don’t know what exactly yet so it just is an empty array at this time []. I guess group doesn’t necessarily have to refer to an array, but it does here.

return group.reduce ( function ( prev, current) {

We’re calling reduce on group, so now the function expects to compare at least two elements to each other and only one champion will survive. I wonder if you can simplify this next part into arrows…
Anyway, this particular reduce method uses two parameters prev, and current, but we don’t know exactly what either of these do yet.

return current>prev ? current: prev;

this line finally specifies using ternary operator that if the current value is greater than the previous value, we keep the current value. Otherwise, we keep the previous value. Whatever is kept to added to the new array created by .map and we toss everything else?

Always a pleasure to meet another HK fan in the wild. :slight_smile:

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:

  1. Each element of the array (optional)
  2. The index of the current element (optional)
  3. 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:

  1. 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.
  2. 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.

image

[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}
2 Likes