denoland / denoland/std

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

Open
#4,386 0 comments 1 reaction 0 assignees View on GitHub
feedback welcome
Dominant language
TypeScript
Stars
3.6k
Forks
681
PR merge metrics
No merged PRs in 30d

Description

#### 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:

```ts
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](https://github.com/tc39/proposal-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.

```ts
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](https://remedajs.com/docs/#pipe), this would allow for:

```ts
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(
(r) => mapKeys(r, (k) => k.toUpperCase()),
(r) => mapValues(r, (v) => v * 2),
);
list.map(pipedTransform);
```

**2. Introduce `data-last` function signatures**

[remeda](https://github.com/remeda/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:

```ts
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).

```ts
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](https://remedajs.com/docs/#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.

```ts
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 😄

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.