Skip to content

Real-world examples of better DX for constructors and factory APIs like Object.fromEntries #314

@justingrant

Description

@justingrant

more empiric evidence for pipe’s impact would be good. (from #232 (comment))

Thanks so much for persevering with this proposal. Here's 8 snippets (I could probably provide hundreds more) from our small company's codebase showing how Pipeline could make it easier to use one common API: Object.fromEntries.

A few surprising (to me) observations from this exercise:

  • Ironically, the biggest usability win was NOT actually moving Object.entries to use Pipeline. Instead, a much larger win was removing calls to reduce (see the last 2 snippets) that were only in our code because reduce is chainable and Object.entries is not. Apparently, some developers at our company love chaining so much that they were willing to write much more code (and much more complicated code) just to avoid having to use a nested function call that breaks method chaining. 🤔 It'd be fun to see a plenary slide that includes the line "Pipeline will reduce reduce". 😄
  • Pipeline and Prettier will likely reinforce each other to improve readability. With nested function calls, each call is indented and moved to a new line by Prettier, and all the extra indents in turn cause function calls to exceed Prettier's line length causing their arguments to also be split across lines. Method chains however (including pipes) are moved to new lines at the same nesting level. The result with Pipeline is not only clearer code that reads right-to-left and top-to-bottom, but it's also less indented so each chained call is much less likely to be split across multiple lines, further improving readability.
  • We don't really use FP. In our codebase, Pipeline would simply make our existing, non-FP code easier to read and easier to write. You don't need to love FP to love Pipeline.

Note that Object.entries is not an outlier. I could provide a similar laundry list of snippets for all popular constructors and factory methods like [...new Set(array)] (for deduping arrays), Temporal.*.from, Array.from, etc. And that's just builtins; any class constructor or factory method in our own code or in libraries we use would get easier to use chainably.

// current
return Object.fromEntries(
  Object.entries(value).map(([k, v]) => [typeof v === 'string' ? `${k}.untranslated` : k, transformValue(v)])
);

// with Pipeline
return Object.entries(value)
  .map(([k, v]) => [typeof v === 'string' ? `${k}.untranslated` : k, transformValue(v)])
  |> Object.fromEntries(%);


// current
const result = Object.fromEntries(
  Object.entries(grouped).map(([machineId, attempts]) => [
    machineId,
    attempts[0],
  ])
);

// with Pipeline
const result = Object.entries(grouped)
  .map(([machineId, attempts]) => [machineId, attempts[0]])
  |> Object.fromEntries(%);


// current
const trimmed = Object.fromEntries(
  Object.entries(payload.message).map(([key, value]) => [
    key,
    value ? value.trim() : unknownLabel,
  ]),
);

// with Pipeline
const trimmed = Object.entries(payload.message)
  .map(([key, value]) => [key, value ? value.trim() : unknownLabel])
  |> Object.fromEntries(%);


// current
const MdIconsTransformedKeys = Object.fromEntries(
  Object.entries(MdIcons).map(([iconName, icon]) => [
    transformIconName(iconName),
    icon,
  ]);

// with Pipeline
const MdIconsTransformedKeys = Object.entries(MdIcons)
  .map(([iconName, icon]) => [transformIconName(iconName), icon])
  |> Object.fromEntries(%);


// current
return Object.fromEntries(
  this.$storeTS.state.printer.printer.material_manager.materials_list.map(m => [m.name, m.color])
);

// with Pipeline
return this.$storeTS.state.printer.printer.material_manager.materials_list
  .map(m => [m.name, m.color])
  |> Object.fromEntries(%);


// current
return Object.entries(localStorage)
  .map(([k, v]) => [String(k), v])
  .filter(([k, _v]) => k.startsWith(FLAG_PREFIX))
  .reduce(
    (acc, [k, v]) => ({ ...acc, [k.slice(FLAG_PREFIX.length)]: !!v }),
    {}
  );

// with Pipeline
return Object.entries(localStorage)
  .map(([k, v]) => [String(k), v])
  .filter(([k, _v]) => k.startsWith(FLAG_PREFIX))
  .map([k, v]) => [k.slice(FLAG_PREFIX.length), !!v])
  |> Object.fromEntries(%);


// current
return Object.entries(colors)
  .toSorted(([_nameA, colorA], [_nameB, colorB]) => {
    const lightnessA = parseToHsl(colorA).lightness;
    const lightnessB = parseToHsl(colorB).lightness;
    return lightnessA - lightnessB;
  })
  .reduce((acc, [name, color]) => {
    acc[name] = color;
    return acc;
  }, {} as Record<string, string>);

// with Pipeline
return Object.entries(colors)
  .toSorted(([_nameA, colorA], [_nameB, colorB]) => {
    const lightnessA = parseToHsl(colorA).lightness;
    const lightnessB = parseToHsl(colorB).lightness;
    return lightnessA - lightnessB;
  })
  |> Object.fromEntries(%) as Record<string, string>);


// current
return Object.keys(state.availableCommands)
  .filter((key) => key.endsWith('CALIBRATE'))
  .reduce((o, key) => {
    return {
      ...o,
      [key]: state.availableCommands[key],
    };
  }, {} as GcodeCommands);

// with Pipeline
return Object.keys(state.availableCommands)
  .filter((key) => key.endsWith('CALIBRATE'))
  .map((key) => [key, state.availableCommands[key]]
  |> Object.fromEntries(%) as GcodeCommands);

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions