Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

suggestion: more readable function chaining for collections (pipe, chain, data-last) #4386

Open
andrewthauer opened this issue Feb 25, 2024 · 0 comments
Labels
feedback welcome We want community's feedback on this issue or PR

Comments

@andrewthauer
Copy link

andrewthauer commented Feb 25, 2024

Is your feature request related to a problem? Please describe

I've recently been trying to use the collections functions and realized that composing multiple calls ends up creating code that is hard to read and maintain due to nested function calls.

It seems these functions were inspired by Kotlin, but the big difference is Kotlin has extension functions so you can chain calls using a fluent api.

Simple example, that just gets more complicated with more function calls:

const list = [
  { a: 1, b: 2, c: 3 },
  { a: 1, b: 3, c: 2 },
];

// NOTE: We could use `mapEntries` here, but this is just a "simple" illustration.
list.map((i) =>
  mapValues(
    mapKeys(i, (k) => k.toUpperCase()),
    (v) => v * 2  // This is in an awkward position for readability
  )
)

I see there have been discussions in the past around this, but it seems like there are no great solutions afaict. The proposed JS pipeline operator (currently stage 2) would potentially solve this, but it seems like a long way off or may never happen. The other options are to try and combine a functional user land library with @std/collections or not use @std at all for this sort of thing.

Describe the solution you'd like

I'm not suggesting that std needs to turn into a full functional like library like ramda, etc. but it would be nice to have something that avoids needing to rely on nested function chaining or reach for another library for common cases.

Supporting point-free piping like below would be ideal, but I realize this is both possibly contentious and unlikely without some drastic API changes.

list.map(i => pipe(mapKeys((k) => k.toUpperCase()), mapValues((v) => v * 2)) 

That said, I think there could be some potential options (one or more) for creating some incremental building blocks that would allow moving in this direction.

1. Introduce a pipe function

Inspired by remeda's pipe, this would allow for:

list.map((r) =>
  pipe(
    (r) => mapKeys(r, (k) => k.toUpperCase()),
    (r) => mapValues(r, (v) => v * 2),
  )
)

// or

const upcaseKeys = (r) => mapKeys(r, (k) => k.toUpperCase());
const double = (r) => mapValues(r, (v) => v * 2);
list.map((r) => pipe(r, upcaseKeys, double));

// or 

const pipedTransform = createPipe<T>(
  (r) => mapKeys(r, (k) => k.toUpperCase()),
  (r) => mapValues(r, (v) => v * 2),
);
list.map(pipedTransform);

2. Introduce data-last function signatures

remeda has a concept of data-first & data-last functions. This allows you to mix and match FP styles with more traditional usage. This would be a way to make the syntax of pipe look like:

list.map((i) =>
  pipe(
    mapKeys((k) => k.toUpperCase()),
    mapValues((v) => v * 2),
  )
)

3. Introduce a chain function

This would be similar to lodash's chain that would wrap the data object and allow for chaining of calls. This would actually provide a similar experience to Kotlin & C# extension functions. Although since it's not a native JS feature, I feel this would only work with functions registered with the wrapper making it less extensible (or require boilerplate to do so).

list.map((i) =>
  chain(i)
    .mapKeys((k) => k.toUpperCase()),
    .mapValues((v) => v * 2),
  )
)

4. Introduce a function that can invert the positional arguments and curry

Remeda has a purry function that creates a function that supports both data-first & data-last signatures and allows for currying of args to support the previous example. The problem is in order to make this properly types you need to create a wrapper function.

export function mapValues2(_transformer /* todo: make this typed */) {
  return purry(_mapValues, arguments);
}

export function mapKeys2(_transformer /* todo: make this typed */) {
  return purry(_mapKeys, arguments);
}

list.map((i) =>
  pipe(
    mapKeys2((k) => k.toUpperCase()),
    mapValues2((v) => v * 2),
  )
)

Describe alternatives you've considered

I'm sure there are alternatives, so I'm eager to here other ideas 😄

@andrewthauer andrewthauer changed the title More readable function chaining for collections (pipe, chain, data-first, data-last, currying, partial application) More readable function chaining for collections (pipe, chain, data-last) Feb 25, 2024
@iuioiua iuioiua added the feedback welcome We want community's feedback on this issue or PR label Mar 3, 2024
@iuioiua iuioiua changed the title More readable function chaining for collections (pipe, chain, data-last) suggestion: more readable function chaining for collections (pipe, chain, data-last) Mar 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feedback welcome We want community's feedback on this issue or PR
Projects
None yet
Development

No branches or pull requests

2 participants