Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Created October 23, 2020 00:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryanflorence/ebbf831df8a05f0438e898f8a52bc7fe to your computer and use it in GitHub Desktop.
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
@loucyx
Copy link

loucyx commented Oct 23, 2020

Both are missing the initialValue for the reduce:

// 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

The actual way of doing this functionally, for those wondering:

/** @param {{ id: string; name: string }[]} items */
const itemsToObject = items => Object.fromEntries(items.map(({ id, name }) => [id, name]));

const obj = itemsToObject(items)

@saykatsu
Copy link

@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.

@loucyx
Copy link

loucyx commented Oct 23, 2020

@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

@pkoch
Copy link

pkoch commented Oct 24, 2020

As Kevlin Henney would say: we ran out of excuses to write loops. Let's stop doing those.

@saykatsu
Copy link

@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.

@loucyx
Copy link

loucyx commented Oct 25, 2020

@saykatsu I'm on the vibe that DRY and KISS are good principles, yes. The thing is:

  1. If your utils/helpers are "buried in their own modules", then you need to review the architecture of the app.
  2. 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.
  3. 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 😄

@pkoch
Copy link

pkoch commented Oct 25, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment