const objectFromPairs = arr => arr.reduce((a, v) => ((a[v[0]] = v[1]), a), {});
console.log(objectFromPairs([['a', 1], ['b', 2]])); // {a: 1, b: 2}
I can't wrap my head around this. What does callback (a, v) => ((a[v[0]] = v[1]), a) do - isn't a reducer's callback supposed to be just a function, why is there assignment followed by a comma then accumulator? How do I make sense of this?
When it's (a, v) => a[v[0]] = v[1], why does it return 2? Shouldn't it return {a: 1} on the first iteration, then {b: 2}, so shouldn't we end up with {b: 2} instead of just 2?
I can't wrap my head around this.
Understandable – it uses the relatively obscure (and proscribed!) comma operator. You can expand it to the equivalent statements to make it more readable:
const objectFromPairs = arr => arr.reduce((a, v) => {
a[v[0]] = v[1];
return a;
}, {});
Still kind of an abuse of reduce, though. This is a generic function; it doesn’t have to be golfed.
const objectFromPairs = pairs => {
const object = {};
pairs.forEach(([key, value]) => {
object[key] = value;
});
return object;
};
Starting from the inside and working out:
(a, v) => ((a[v[0]] = v[1]), a)
That's a reduce callback that takes an "accumulator" parameter (a) and a "value" parameter (v). What the body of the function does is employ a comma operator expression statement to get around the need to use curly braces and an explicit return. The first subexpression in the comma operator expression, (a[v[0]] = v[1]), splits up a two-value array into a name for an object property and a value. That is, v[0] becomes a name for a property in the accumulator object, and v[1] is that property's value.
Now that's used in a .reduce() call made on an array, with {} as the initial value for the .reduce() accumulator. Thus, that function builds up properties with values taken from the source array, whose elements clearly need to be arrays themselves, because that's what the callback expects.
It looks like the key to your confusion is that comma operator. An arrow function in JavaScript only has an implied return value when its function body is just a single expression. The comma operator is a way to "cheat" a little bit: you can string together several expressions, with the overall result being the value of the last expression. For an arrow function, that's handy.
Related
I'm currently learning about the reduce method in JS, and while I have a basic understanding of it, more complex code completely throws me off. I can't seem to wrap my head around how the code is doing what it's doing. Mind you, it's not that the code is wrong, it's that I can't understand it. Here's an example:
const people = [
{ name: "Alice", age: 21 },
{ name: "Max", age: 20 },
{ name: "Jane", age: 20 },
];
function groupBy(objectArray, property) {
return objectArray.reduce((acc, obj) => {
const key = obj[property];
const curGroup = acc[key] ?? [];
return { ...acc, [key]: [...curGroup, obj] };
}, {});
}
const groupedPeople = groupBy(people, "age");
console.log(groupedPeople);
// {
// 20: [
// { name: 'Max', age: 20 },
// { name: 'Jane', age: 20 }
// ],
// 21: [{ name: 'Alice', age: 21 }]
// }
Now the reduce method as I understand it, takes an array, runs some provided function on all the elements of the array in a sequential manner, and adds the result of every iteration to the accumulator. Easy enough. But the code above seems to do something to the accumulator as well and I can't seem to understand it. What does
acc[key] ?? []
do?
Code like this make it seem like a breeze:
const array1 = [1, 2, 3, 4];
// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array1.reduce(
(accumulator, currentValue) => accumulator + currentValue,
initialValue
);
console.log(sumWithInitial);
// Expected output: 10
But then I see code like in the first block, I'm completely thrown off. Am I just too dumb or is there something I'm missing???
Can someone please take me through each iteration of the code above while explaining how it
does what it does on each turn? Thanks a lot in advance.
You are touching on a big problem with reduce. While it is such a nice function, it often favors code that is hard to read, which is why I often end up using other constructs.
Your function groups a number of objects by a property:
const data = [
{category: 'catA', id: 1},
{category: 'catA', id: 2},
{category: 'catB', id: 3}
]
console.log(groupBy(data, 'category'))
will give you
{
catA: [{category: 'catA', id: 1}, {category: 'catA', id: 2}],
catB: [{category: 'catB', id: 3}]
}
It does that by taking apart the acc object and rebuilding it with the new data in every step:
objectArray.reduce((acc, obj) => {
const key = obj[property]; // get the data value (i.e. 'catA')
const curGroup = acc[key] ?? []; // get collector from acc or new array
// rebuild acc by copying all values, but replace the property stored
// in key with an updated array
return { ...acc, [key]: [...curGroup, obj] };
}, {});
You might want to look at spread operator (...) and coalesce operator (??)
Here is a more readable version:
objectArray.reduce((groups, entry) => {
const groupId = entry[property];
if(!groups[groupId]){
groups[groupId] = [];
}
groups[groupId].push(entry);
return groups;
}, {});
This is a good example where I would favor a good old for:
function groupBy(data, keyProperty){
const groups = {}
for(const entry of data){
const groupId = entry[keyProperty];
if(!groups[groupId]){
groups[groupId] = [];
}
groups[groupId].push(entry);
}
return groups;
}
Pretty much the same number of lines, same level of indentation, easier to read, even slightly faster (or a whole lot, depending on data size, which impacts spread, but not push).
That code is building an object in the accumulator, starting with {} (an empty object). Every property in the object will be a group of elements from the array: The property name is the key of the group, and the property value is an array of the elements in the group.
The code const curGroup = acc[key] ?? []; gets the current array for the group acc[key] or, if there isn't one, gets a new blank array. ?? is the "nullish coalescing operator." It evaluates to its first operand if that value isn't null or undefined, or its second operand if the first was null or undefined.
So far, we know that obj[property] determines the key for the object being visited, curGroup is the current array of values for that key (created as necessary).
Then return { ...acc, [key]: [...curGroup, obj] }; uses spread notation to create a new accumulator object that has all of the properties of the current acc (...acc), and then adds or replaces the property with the name in key with a new array containing any previous values that the accumulator had for that key (curGroup) plus the object being visited (obj), since that object is in the group, since we got key from obj[property].
Here's that again, related to the code via comments. I've split out the part creating a new array [...curGroup, obj] from the part creating a new accumulator object for clarity:
function groupBy(objectArray, property) {
return objectArray.reduce(
(acc, obj) => {
// Get the value for the grouping property from this object
const key = obj[property];
// Get the known values array for that group, if any, or
// a blank array if there's no property with the name in
// `key`.
const curGroup = acc[key] ?? [];
// Create a new array of known values, adding this object
const newGroup = [...curGroup, obj];
// Create and return a new object with the new array, either
// adding a new group for `key` or replacing the one that
// already exists
return { ...acc, [key]: newGroup };
},
/* The starting point, a blank object: */ {}
);
}
It's worth noting that this code is very much written with functional programming in mind. It uses reduce instead of a loop (when not using reduce, FP usually uses recursion rather than loops) and creates new objects and arrays rather than modifying existing ones.
Outside of functional programming, that code would probably be written very differently, but reduce is designed for functional programming, and this is an example of that.
Just FWIW, here's a version not using FP or immutability (more on immutability below):
function groupBy(objectArray, property) {
// Create the object we'll return
const result = {};
// Loop through the objects in the array
for (const obj of objectArray) {
// Get the value for `property` from `obj` as our group key
const key = obj[property];
// Get our existing group array, if we have one
let group = result[key];
if (group) {
// We had one, add this object to it
group.push(obj);
} else {
// We didn't have one, create an array with this object
// in it and store it on our result object
result[key] = [obj];
}
}
return result;
}
In a comment you said:
I understand the spread operator but it's use in this manner with the acc and the [key] is something I'm still confused about.
Yeah, there are a lot of things packed into return { ...acc, [key]: [...curGroup, obj] };. :-) It has both kinds of spread syntax (... isn't an operator, though it's not particularly important) plus computed property name notation ([key]: ____). Let's separate it into two statements to make it easier to discuss:
const updatedGroup = [...curGroup, obj];
return { ...acc, [key]: updatedGroup };
TL;DR - It creates and returns a new accumulator object with the contents of the previous accumulator object plus a new or updated property for the current/updated group.
Here's how that breaks down:
[...curGroup, obj] uses iterable spread. Iterable spread spreads out the contents of an iterable (such as an array) into an array literal or a function call's argument list. In this case, it's spread into an array literal: [...curGroup, obj] says "create a new array ([]) spreading out the contents of the curGroup iterable at the beginning of it (...curGroup) and adding a new element at the end (, obj).
{ ...acc, ____ } uses object property spread. Object property spread spreads out the properties of an object into a new object literal. The expression { ...acc, _____ } says "create a new object ({}) spreading out the properties of acc into it (...acc) and adding or updating a property afterward (the part I've left as just _____ for now)
[key]: updatedGroup (in the object literal) uses computed property name syntax to use the value of a variable as the property name in an object literal's property list. So instead of { example: value }, which creates a property with the actual name example, computed property name syntax puts [] around a variable or other expression and uses the result as the property name. For instance, const obj1 = { example: value }; and const key = "example"; const obj2 = { [key]: value }; both create an object with a propety called example with the value from value. The reduce code is using [key]: updatedGroup] to add or update a property in the new accumulator whose name comes from key and whose value is the new group array.
Why create a new accumulator object (and new group arrays) rather than just updating the one that the code started with? Because the code is written such that it avoids modifying any object (array or accumulator) after creating it. Instead of modifying one, it always creates a new one. Why? It's "immutable programming," writing code that only ever creates new things rather than modifying existing things. There are good reasons for immutable programming in some contexts. It reduces the possibilities of a change in code in one place from having unexpected ramifications elsewhere in the codebase. Sometimes it's necessary, because the original object is immutable (such as one from Mongoose) or must be treated as though it were immutable (such as state objects in React or Vue). In this particular code it's pointless, it's just style. None of these objects is shared anywhere until the process is complete and none of them is actually immutable. The code could just as easily use push to add objects to the group arrays and use acc[key] = updatedGroup; to add/update groups to the accumulator object. But again, while it's pointless in this code, there are good uses for immutable programming. Functional programming usually adheres to immutability (as I understand it; I haven't studied FP deeply).
I have a javascript question:
I was doing some challenge exercises from a JS book I'm learning from and I have no idea how to go about it and I don't have the sample answers. The aim is to write a deep copy function using *only ES6*'s arrow functions without using any JSON methods or any variable assignments. The author also suggests no scopes using the {} brackets. Any hints or tips on how I can do this?
Create a recursive function which accepts a value and will return a copy of it. Check the type of the value. If it's an Array, return an new Array which maps every element to a recursive call of that function. If it's an Object, create a new Object with copies of its properties. If it's primitive (string, number, boolean...), just return it.
On a side note, writing this without any variable assignment & curly brackets may be fun for learning, but it makes it a lot less readable. I would never write it this way at work:
const deepCopy = v =>
// Is it an Array?
v instanceof Array ?
// If it is, map every value to its copy
v.map(deepCopy) :
// Otherwise, is it a non-null Object?
(typeof v === "object" && v !== null) ?
// If it is, create a new Object with copies of its property values
Object.entries(v)
.reduce((acc, [k, v]) => ({ ...acc, [k]: deepCopy(v) }), {}) :
// Otherwise, just return the value
v;
console.log(deepCopy({foo: 'bar', arr: [{baz: 42}, true]}));
console.log(deepCopy("Hello world"));
console.log(deepCopy(["a", "b", null]));
You can get arrays of keys and values with Object.keys() and Object.values(), then iterates and assigns those items to a new object, try this:
let obj = {key1: 'value1', key2: 'value2'};
let keys = Object.keys(obj);
let values = Object.values(obj)
console.log(keys, values)
let newObj = {};
for( let i = 0; i < keys.length; i++ ){
newObj[keys[i]] = values[i]
}
console.log(newObj);
newObj.key1 = 'asd';
console.log(newObj);
I am having a little trouble differentiating between spread and rest. Could someone explain to me if either spread or rest is being used within the reduce functions return statement?
This is what I am not understanding:
return [currentValue, ...accumulator]
inside of
let str = 'KING';
const reverseStr = str => {
return str
.split('')
.reduce((accumulator, currentValue) => {
return [currentValue, ...accumulator];
}, [])
.join('');
};
Rest syntax always creates (or assigns to) a variable, eg:
const [itemOne, ...rest] = arr;
// ^^ created ^^^^
Spread syntax only results in another array (or object) expression - it doesn't put anything into variables. Here, you're using spread syntax to create a new array composed of currentValue and the values of accumulator.
return [currentValue, ...accumulator];
is like
return [currentValue, accumulator[0], accumulator[1], accumulator[2] /* ... */];
You're spreading the items of accumulator into the array that's being returned.
not sure what is happening in .map(([key, value]) => [key, value * 2]) I have a gist idea of what the square brackets do in that parameter. But can someone clarify the rules of the syntax, or explain whats happening?
Here's my belief of how the code works:
Object.entries converts object prices to array. returns something like [[banana, 1], [orange, 2], [meat, 4]]
map is an array-method and the function doubles the prices of each item.
the chagnes are then reconverted back to object.
if map's going through the items; would it make more sense to write .map((item) => [item[0], item[1] * 2])
going through a lesson from javascript.info
let prices = {
banana: 1,
orange: 2,
meat: 4,
};
let doublePrices = Object.fromEntries(
// convert to array, map, and then fromEntries gives back the object
Object.entries(prices).map(([key, value]) => [key, value * 2])
);
console.log(doublePrices.meat); // 8
In the code snippet you're asking about the left side is quite different from the right side. Let's break it down:
.map(([key, value]) => [key, value * 2])
First, map takes a function as its parameter. Here the function is essentially doing this:
(entry) => {
const key = entry[0];
const value = entry[1];
const modified_entry = [ key, value * 2 ];
return modified_entry;
}
A shorter way to write that is:
(entry) => [ entry[0], entry[1] * 2 ]
That works the same because without curly brackets around the function body, it uses the value as the return value.
Now the code you're asking about isn't using a single variable for the input parameter to the function. It's a single parameter, but it sets 2 variables by using something called deconstruction or destructuring. This is a feature in Javascript that lets you break apart an object into multiple variables in a simple way. For example look at this:
var entry = [ 'meat', 1]
var [ item, price ] = entry
// now item is 'meat', and price is 1, which is nicer than having to do this:
item = entry[0]
price = entry[1]
So getting back to your code, they've expressed entry as [key,value] so that they can be used in the function easily, and on the right hand side of the => we have the return value which is a new array with the doubled price.
Remember that on the left side we're breaking an object into new variables which get assigned values, while on the right side we're creating a new Array object that's being returned. It can be a bit confusing because they both have the same form of looking like an array in square brackets.
Note also that deconstruction works for objects as well as arrays, so this is valid code too:
var obj = { a:1, b:2, c:3 }
var { a, c } = obj
console.log(a,c) // will print "1 3"
Deconstructing an object lets you take just the parts you want out of the object and creates variables named after the key. Notice that we didn't ask for b so there's no b variable - it's just ignored.
I'm trying to use the array reduce function to return a 2D array of objects. The input is a comma separated values. The first row of the string is used as the title row. I'm analyzing the solution and I don't understand the notation. Specifically I don't understand the "=> ((obj[title] =
values[index]), obj), {})" portion in the code below. I'm looking to have someone explain it to me. For me it seems like we're initializing obj to be a an object. After that I'm lost.
const CSV_to_JSON = (data, delimiter = ',') => {
const titles = data.slice(0, data.indexOf('\n')).split(delimiter);
return data
.slice(data.indexOf('\n') + 1)
.split('\n')
.map(v => {
const values = v.split(delimiter);
return titles.reduce(
(obj, title, index) => ((obj[title] = values[index]), obj)
, {});
});
};
console.log(CSV_to_JSON('col1,col2\na,b\nc,d')); // [{'col1': 'a', 'col2': 'b'}, {'col1': 'c', 'col2': 'd'}];
console.log(CSV_to_JSON('col1;col2\na;b\nc;d', ';')); // [{'col1': a', 'col2': 'b'}, {'col1': 'c', 'col2': 'd'}]
It's an (ab)use of the comma operator, which takes a list of comma-separated expressions, evaluates the first expression(s), discards them, and then the whole (...) resolves to the value of the final expression. It's usually something only to be done in automatic minification IMO, because the syntax looks confusing.
The .reduce there
return titles.reduce((obj, title, index) => ((obj[title] =
values[index]), obj), {});
is equivalent to
return titles.reduce((obj, title, index) => {
obj[title] = values[index];
return obj;
}, {});
which makes a lot more sense - it turns an array of titles (eg ['foo', 'bar']) and an array of values (eg ['fooVal', 'barVal']), and uses .reduce to transform those into a single object, { foo: 'fooVal', bar: 'barVal' }.
The first argument to the .reduce callback is the accumulator's initial value (the second argument to .reduce), or the value that was returned on the last iteration - the code above passes {} as the initial value, assigns a property to the object, and returns the object on every iteration. .reduce is the usually the most appropriate method to use to turn an array into an object, but if you're more familiar with forEach, the code is equivalent to
const obj = {};
titles.forEach((title, index) => {
obj[title] = values[index];
});
return obj;
While the comma operator can be useful when code-golfing, it's probably not something that should be used when trying to write good, readable code.