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

feat(rx): A new method of piping has been added #7229

Merged
merged 7 commits into from May 16, 2023
Merged

Conversation

benlesh
Copy link
Member

@benlesh benlesh commented Mar 23, 2023

  • Adds rx a new method of piping observables. This allows any valid observable input to be passed as the first argument, and it will implicitly convert the argument to an Observable and pass it to the function that may be present in the second argument, the return value of THAT function is then passed an argument to the next function, and so on
  • Corrects the types of Observable#pipe such that anything can be returned at any step of the functional chain, however the first pipeable function must accept an Observable<T>.
  • Adds dtslint and runtime tests for rx and pipe.

NOTE: This does NOT deprecateObservable#pipe. That will be done in a follow up, and we need to define what timeline we'll take to remove that, as it's an API that is broadly used - could be v9, could be v10, could be never. At this point, this is alpha/beta functionality.

BREAKING CHANGE: Observable#pipe now allows any pipeable unary function as an argument, just so long as the types properly compose, this means in some cases it will now return unknown instead of Observable<unknown> the workaround is just to cast the result for those cases. (or you can break your operator piping into smaller pipe sets)

Addresses #7203

Note for later: Make sure to update squashed commit message to reflect rx and not r.

@benlesh benlesh added the 8.x Issues and PRs for version 8.x label Mar 23, 2023
+ Adds `r` a new method of piping observables. This allows any valid observable input to be passed as the first argument, and
 it will implicitly convert the argument to an Observable and pass it to the function that may be present in the second argument, the
 return value of THAT function is then passed an argument to the next function, and so on
+ Corrects the types of `Observable#pipe` such that anything can be returned at any step of the functional chain, however the first pipeable function must accept an `Observable<T>`.
+ Adds dtslint and runtime tests for `r` and `pipe`.

NOTE: This does NOT deprecate`Observable#pipe`. That will be done in a follow up, and we need to define what timeline we'll take to remove that, as it's an API that is broadly used - could be v9, could be v10, could be never. At this point, this is alpha/beta functionality.

BREAKING CHANGE: `Observable#pipe` now allows any pipeable unary function as an argument, just so long as the types properly compose, this means in some cases it will now return `unknown` instead of `Observable<unknown>` the workaround is just to cast the result for those cases. (or you can break your operator piping into smaller pipe sets)

Related ReactiveX#7203
src/internal/util/r.ts Outdated Show resolved Hide resolved
@benlesh
Copy link
Member Author

benlesh commented Mar 23, 2023

After some Core Team discussion, and some testing of the syntax, we've decided to move the implementation to where it takes an array of operators as the second argument. One of the example cases that compelled this change was this:

r(
  combineLatest([a, b]),
  map(([a, b]) => a + b),
  filter(n => n > 10),
)
.subscribe(console.log)

In the above scenario, muscle-memory for RxJS users leads them to believe that combineLatest is an operator, where it is in fact creating an observable.

The following was deemed to be more readable:

r(
  combineLatest([a, b]),
  [
    map(([a, b]) => a + b),
    filter(n => n > 10),
  ],
)
.subscribe(console.log)

This PR is currently broken

I knowingly pushed it up this way so it can be addressed later. There's a problem with the TypeScript types when we do this. TypeScript can't seem to figure out how to type the operators as they go through the array.

Actions

  1. Explore alternative APIs? like: r({ source, operators }), this is too verbose and ends up with the same issues the array does.
  2. Engage the TypeScript team to see if there's a better way to do this. Possibly some recursive type shenanigans?
  3. Roll back to the rest arguments implementation as "good enough".

@benlesh
Copy link
Member Author

benlesh commented Mar 23, 2023

cc @DanielRosenwasser 🙏❤️

@jannispl
Copy link

jannispl commented Apr 2, 2023

Personally I am not a huge fan of the new array notation. One of my main issues when dealing with RxJS operators is getting the damn brackets right when refactoring code. Introducing new ones won't help.

@benlesh
Copy link
Member Author

benlesh commented Apr 10, 2023

After speaking offline with the TypeScript folks, there's just no way to properly type the array notation. So this is going back to the more "flat" approach.

@benlesh
Copy link
Member Author

benlesh commented Apr 10, 2023

I know we talked about this @kwonoj ... but I'm really not sure I love r. lol... 😬

Copy link
Collaborator

@cartant cartant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just a few questions about any usage and a nitpick.

spec/util/pipe-spec.ts Outdated Show resolved Hide resolved
op6: UnaryFunction<E, F>,
op7: UnaryFunction<F, G>,
op8: UnaryFunction<G, H>,
op9: UnaryFunction<H, Observable<I>>,
...operations: OperatorFunction<any, any>[]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we replace the any usage here with unknown?

op7: UnaryFunction<F, G>,
op8: UnaryFunction<G, H>,
op9: UnaryFunction<H, I>,
...operations: UnaryFunction<any, any>[]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we replace the any usage here with unknown?

@@ -344,7 +350,7 @@ export class Observable<T> implements Subscribable<T> {
* .subscribe(x => console.log(x));
* ```
*/
pipe(...operations: OperatorFunction<any, any>[]): Observable<any> {
pipe(...operations: UnaryFunction<any, any>[]): unknown {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we replace the any usage here with unknown?

fn7: UnaryFunction<F, G>,
fn8: UnaryFunction<G, H>,
fn9: UnaryFunction<H, I>,
...fns: UnaryFunction<any, any>[]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as with pipe: Can we replace the any usage here with unknown?

Copy link
Member

@kolodny kolodny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀🚀🚀

...operations: OperatorFunction<any, any>[]
): Observable<unknown>;
pipe<A, B, C, D, E, F, G, H, I>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really wish we either had a dedicated pipe operator in js or at least had some way to use recursive/conditional types powerful enough to support not needing to do this.

src/internal/util/rx.ts Outdated Show resolved Hide resolved
* ```ts
* r(source, map(x => x + 1), filter(x => x % 2 === 0));
* pipe(from(source), map(x => x + 1), filter(x => x % 2 === 0));
* pipe(source, from, map(x => x + 1), filter(x => x % 2 === 0));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably also worth calling out that rx([1, 2, 3]); is an Observable<number> and not Observable<number[]> in the documentation (it's already in the type tests)

Copy link
Member

@jakovljevic-mladen jakovljevic-mladen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of minor issues found: r to rx rename should be updated in couple of places, some generics issues need to be addressed and docs needs more info and some updates.

src/internal/Observable.ts Outdated Show resolved Hide resolved
import { ObservableInput, UnaryFunction } from '../types';
import { pipeFromArray } from './pipe';

export function rx<T, A>(source: ObservableInput<A>): Observable<A>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The T generics are unused in the entire file. Please remove them.

src/internal/util/rx.ts Outdated Show resolved Hide resolved
src/internal/util/rx.ts Outdated Show resolved Hide resolved
* pipe(source, from, map(x => x + 1), filter(x => x % 2 === 0));
* ```
*
* Furthermore, `r` can be used to create an observable and pipe it in any number of ways. For example:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Furthermore, `r` can be used to create an observable and pipe it in any number of ways. For example:
* Furthermore, `rx` can be used to create an observable and pipe it in any number of ways. For example:

Comment on lines 6 to 8
export function rx<T, A>(source: ObservableInput<A>): Observable<A>;
export function rx<T, A, B>(source: ObservableInput<A>, fn2: UnaryFunction<Observable<A>, B>): B;
export function rx<T, A, B, C>(source: ObservableInput<A>, fn2: UnaryFunction<Observable<A>, B>, fn3: UnaryFunction<B, C>): C;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The T generics are unused in the entire file. Please remove them.

Or use them ☝️ like this:

Suggested change
export function rx<T, A>(source: ObservableInput<A>): Observable<A>;
export function rx<T, A, B>(source: ObservableInput<A>, fn2: UnaryFunction<Observable<A>, B>): B;
export function rx<T, A, B, C>(source: ObservableInput<A>, fn2: UnaryFunction<Observable<A>, B>, fn3: UnaryFunction<B, C>): C;
export function rx<T>(source: ObservableInput<T>): Observable<T>;
export function rx<T, A>(source: ObservableInput<T>, fn1: UnaryFunction<Observable<T>, A>): A;
export function rx<T, A, B>(source: ObservableInput<T>, fn1: UnaryFunction<Observable<T>, A>, fn2: UnaryFunction<A, B>): B;

...and so on.

FWIW: starting with T and fn1 (instead of fn2) is much better IMO as users might wonder where fn1 is when reviewing the docs.

This is the docs as I suggested here:

image

vs how it is now:

image

*
* Furthermore, `r` can be used to create an observable and pipe it in any number of ways. For example:
*
* ```ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order for these examples to work on StackBlitz, I'd like to introduce imports:

Suggested change
* ```ts
* ```ts
* import { rx, of } from 'rxjs';

* },
* });
* ````
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add @param args ... and @return ... here. Docs seem empty for now:

image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the types up, which should resolve that a little.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also added @param and @return docs.

*
* ```ts
* r(source, map(x => x + 1), filter(x => x % 2 === 0));
* pipe(from(source), map(x => x + 1), filter(x => x % 2 === 0));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be valid. This does:

Suggested change
* pipe(from(source), map(x => x + 1), filter(x => x % 2 === 0));
* pipe(() => from(source), map(x => x + 1), filter(x => x % 2 === 0));

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. It was a little different than what that said.

*
* The following are equivalent:
*
* ```ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the source would help a lot. E.g.

const source = Promise.resolve(1);

or

const source = [1, 2, 3];

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added more context.

@jakovljevic-mladen jakovljevic-mladen changed the title feat(r): A new method of piping has been added feat(rx): A new method of piping has been added Apr 26, 2023
benlesh and others added 5 commits May 15, 2023 16:57
Co-authored-by: Mladen Jakovljević <jakovljevic.mladen@gmail.com>
Co-authored-by: Nicholas Jamieson <nicholas@cartant.com>
Co-authored-by: Mladen Jakovljević <jakovljevic.mladen@gmail.com>
Co-authored-by: Mladen Jakovljević <jakovljevic.mladen@gmail.com>
+ Adds some more tests around `Observable#pipe`, ensuring arbitrary unary functions work
+ Adds more dtslint tests for `rx`.
+ Fixes some comments
+ Improves types and runtime for args in `rx` function
import { pipeFromArray } from './pipe';

export function rx<A>(source: ObservableInput<A>): Observable<A>;
export function rx<A, B>(source: ObservableInput<A>, fn2: UnaryFunction<Observable<A>, B>): B;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still in favor of using fn1 instead of fn2 here.

Copy link
Member

@jakovljevic-mladen jakovljevic-mladen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall, there are just some nitpicks that I'd like to be considered fixing.

@benlesh benlesh merged commit c2cf0c4 into ReactiveX:master May 16, 2023
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
8.x Issues and PRs for version 8.x
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants