ReactiveX / IxJS

The Interactive Extensions for JavaScript
https://reactivex.io/IxJS/
MIT License
1.33k stars 74 forks source link

Proposal: add pipeable versions of functions like toArray() or reduce() #294

Open millimoose opened 4 years ago

millimoose commented 4 years ago

Currently, only functions that both take and return an Iterable (or AsyncIterable) are available as pipeable operators; functions that take an Iterable and return some "scalar" value are not.

This means that e.g. code that uses piping that needs to return an array - which is common in my codebase because very little third-party JS code works with Iterables - has to be written like this:

selectedCheckboxes(): string[] {
    const result = ix.from(Object.entries(this.form.controls)).pipe(
        ixop.filter(([, ctl]) => !!ctl.value),
        ixop.map(([key]) => key),
    );
    return ix.toArray(result);
}

as opposed to, hypothetically, like this:

selectedCheckboxes(): string[] {
    ix.from(Object.entries(this.form.controls)).pipe(
        ixop.filter(([, ctl]) => !!ctl.value),
        ixop.map(([key]) => key),
        ixop.toArray(),
    );
}

(Example is getting the keys of selected checkboxes from an Angular form.)

This can be worked around by using a utility library with a more generic pipe(), and in a future an unspecified time away may be made irrelevant by the upcoming native pipe operator proposals, but it'd be nice if Ix provided this in the meantime, given the overall benefits of piping for readability and chaining.

Using reduce() as an example function to be converted, I propose:

  1. Adding a new operator type:
    type TerminalOperator<T, R> = UnaryFunction<Iterable<T>, R>
  2. Adding new overloads to pipe() that add a parameter of this type at the end, e.g.:
    function pipe<T, A, R>(source: Iterable<T>, op1: OperatorFunction<T, A>, term: TerminalOperator<A, R>): R;
  3. Adding operator versions of functions that take an Iterable as their first parameter but don't return an Iterable:

ix/iterable/operators/reduce.ts

import { reduce as _reduce } from '../reduce';
function reduce<T, R = T>(
  accumulator: (previousValue: R, currentValue: T, currentIndex: number) => R,
  seed?: R
): TerminalOperator<T, R> {
  return function reduceOperatorFunction(source: Iterable<T>) {
    return _reduce(source, accumulator, seed);
  };
}

It's possible to eliminate some of the gruntwork in the last step by using a generic function like:

function toOperator<T, F extends (first: Iterable<T>, ...rest: any[]) => any>(
    fn: F
): (...rest: any[]) => TerminalOperator<T, ReturnType<F>> {
    return (...rest) => first => fn(first, ...rest);

but it seems impossible to use it as a workaround directly, Typescript is unable to infer the types correctly; therefore it's necessary to provide the actual signatures of the resulting operators for this to be of much use.

Given that this is mostly mechanical work, I might be up to actually contributing this if there's interest, but for obvious reasons I'd like to gauge that before making a huge PR.