-
-
Save ryanflorence/ebbf831df8a05f0438e898f8a52bc7fe to your computer and use it in GitHub Desktop.
// don't clutter up lines with function blocks | |
let obj = items.reduce((memo, item) => { | |
memo[item.id] = item.name; | |
return memo; | |
}); | |
// just use the comma operator for super clean code! | |
let obj = items.reduce((memo, item) => (memo[item.id] = item.name, memo)); | |
// lol |
@lukeshiru いや my dude, reduce does not require an initial value.
and the purest and most functional witch-like incantation would actually look like this, for those whondering:
let itemsToObject = (items) => {
let result = {}
for (let item of items) {
result[item.id] = item.name
}
return result
}
The use of let
makes this a purer function since their are less characters cluttering our source code.
@saykatsu to each their own, I guess. I made an entire post on the topic, but my code can be split even further into reusable and composable parts:
const entriesToObject = entries => Object.fromEntries(entries);
const map => mapper => mappable => mappable.map(mapper);
const mapToObject => mapper => mappable => entriesToObject(map(mapper)(mappable));
Now, you could argue that I had to write more code, but I had to do that once. Now every time I need to map an array (or anything that has a map
method in it) to an object, I just need to use mapToObject
, yours needs to be tailored to every object you have.
So we end up with this:
const fooToObject = mapToObject(({ id, foo }) => [id, foo]);
const barToObject = mapToObject(({ id, bar }) => [id, bar]);
const bazToObject = mapToObject(({ id, baz }) => [id, baz]);
Versus this (I did my best to make your version "shorter"):
let fooToObject = items => {
let result = {};
for (let { id, foo } of items) result[id] = foo;
return result;
};
let barToObject = items => {
let result = {};
for (let { id, bar } of items) result[id] = bar;
return result;
}
let bazToObject = items => {
let result = {};
for (let { id, baz } of items) result[id] = baz;
return result;
}
We could replace maybe the body of the for
to use a function, but still we have that unnecessary boilerplaite for
all over the place 😕
PS: Array.prototype.reduce
doesn't need an initialValue
, but for this particular scenario it was needed, because if not then: https://twitter.com/ryanflorence/status/1319517031809478657
As Kevlin Henney would say: we ran out of excuses to write loops. Let's stop doing those.
@lukeshiru I think you're on the vibe that shorter reusable code is somehow better than specific code that does one job. You can def write all these helpers and make use of curried function APIs but when you're working with a team of engineers I think it best to write simple code that you can understand at a glance.
usually these functional helpers are buried in their own modules, causing a developer to jump around multiple files and dig deep through a stack-trace vs looking at line X of a single function.
@saykatsu I'm on the vibe that DRY and KISS are good principles, yes. The thing is:
- If your utils/helpers are "buried in their own modules", then you need to review the architecture of the app.
- If the developers need to "jump around multiple files" to understand the util, then you need to review the util name, its JSDocs/types, etc. A good util is the one that doesn't need an explanation, so if the dev needs to check the implementation to understand it, is not a good util.
- You said it yourself, the idea is to have simple code that anyone can read, and from my point of view...
// I read this as "map to an object the given items", I don't care about the actual implementation...
const itemsToObject = mapToObject(({ id, name }) => [id, name]);
// And I read this as: "ok ... so I take items, what do I do with them?"
const itemsToObject = items => {
// ok so now I'm creating a "result" empty object...
let result = {};
// I loop trough the items and change "result" on every iteration...
for (let { id, name } of items) result[id] = name;
// And I return "result"..... Oh! So is mapping values to an object!
return result;
}
... having that behavior abstracted away in a function makes it far easier to test, maintain, read, and so on 😄
I'd also like to point out there's a strong factor of education and familiarity here. It took me as long to understand both lukeshiru's snippets. Not because I'm ✨ mEgA ✨ SmaRT ✨ but just because I'm already accustomed to both styles. Familiarity is a hurdle to overcome to be able to get our hands on more tools.
I think the best way of thinking about this is as if it was another "sub-language" with its own standard library. Bear with me for a little while. For example, in a functional code base, I'd expect some concepts to be available. Namely:
const pluck = (...keys) => obj => keys.map(k => obj[key])
const map => f => iterable => iterable.map(f)
const pipe => (...fns) => fns.reduce((a, f) => x => f(a(x)), x => x)
const pipeValue => (obj, ...fns) => pipe(...fns)(obj)
I can already hear you say "pkoch, that's a bunch of arrow soup, and it's not helping me". To which I say, well, yes, for now, but that was the alien part (and it's something that you can code and test in an hour). You only really need to know what the names are, and they let me pull off the following trick.
You know how everyone gets their panties in a knot when they see Elixir code like this:
items
|> Enum.map(Map.take(&1, [:id, :name]))
|> Map.new
Well, with the functions I've introduced, you can do something similar. It looks like this:
pipeValue(
items,
map(pluck(['id', 'name'])),
Object.fromEntries,
)
I will never think any for loop is more readable than this pipeValue
version. Perhaps the reverse is true for you, but I hope I have demonstrated why this style works well for me.
Both are missing the
initialValue
for thereduce
:The actual way of doing this functionally, for those wondering: