-
Notifications
You must be signed in to change notification settings - Fork 3k
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(operate): Adds a .operate() and alias .o() operator #2034
Conversation
How does TypeScript handle this when it comes to operators like |
It seems like the first argument should be |
@Blesh not entirely sure of your first question regarding groupBy, but generally best I can tell when using things like Edit: unless we add every single type signature for all operators, which at first seems a little overboard but perhaps... |
@jayphelps, you should be able to use generics for this. |
@Blesh I'm still not following. Can you show me what you mean using a more complete example maybe? Add your suggested generics to: stream.operate(map, i => i + 1) |
I just mean for signature overrides. I'll add more when I'm not on my phone. |
This is quite intriguing but has one flaw - the types won't flow properly so you will not get operator-specific checking. I don't know enough about how the operators are internally wired together (I remember something about
maybe even:
// cc: @chuckjaz - for other typescript ideas on how to get the types to flow properly. |
@Igorbek yep, the lack of type safety is just like This sort of thing: of(1, 2, 3)
.o(filter(i => i > 1))
.o(map(i => i * 10))
.subscribe(x => console.log(x)); or similar wouldn't work (currently) because operators require // .map() signature
o<T>(operator: typeof map, project: (value: T, index: number) => R, thisArg?: any): Observable<T>;
// .filter() signature
o<T>(operator: typeof filter, predicate: (value: T, index: number) => boolean, thisArg?: any): Observable<T>;
// etc
o<T>(operator: Function, ...args: Array<any>): Observable<T> {
return operator.apply(this, args);
} It's possible something like your suggestion is more ergonomic though. Not immediately clear for me. of(1, 2, 3)
.o(
filter(i => i > 1),
map(i => i * 10)
)
.subscribe(x => console.log(x)); Either way I hate having to maintain duplicate signatures like this, especially in another file. I have a hunch these will get out of sync--but maybe I'm being paranoid. Personally I'm leaning towards lack of type safety since it's no worse than the existing |
I see. This would work as well. If I have custom operators, I should be On Thu, Oct 13, 2016 at 9:25 PM Jay Phelps notifications@github.com wrote:
|
Why not just stay with |
Because let is rather verbose to use in this way IMO and the number one reason is that it is not included on the prototype by default--so you can't add it or you're defeating the point. We could consider making it added always by default though. |
Worst case we should be able to augment the operator on |
I remember mentioning something on slack, but it's worth raising perhaps. If we updated the operators to not use The only difference between (as of TypeScript 2.0) is the argument name and the call to export function map<T, R>(this: Observable<T>, project: (value: T, index: number) => R, thisArg?: any): Observable<R> {
return this.lift(new MapOperator(project, thisArg));
}
// vs
export function map<T, R>(context: Observable<T>, project: (value: T, index: number) => R, thisArg?: any): Observable<R> {
return context.lift(new MapOperator(project, thisArg));
} With this we would see something like... import { of } from 'rxjs/observable/of';
import { filter } from 'rxjs/operator/filter';
import { map } from 'rxjs/operator/map';
map(
filter(
of(1, 2, 3),
i => i > 1
),
i => i * 10
)
.subscribe(x => console.log(x)); Now that is clearly a little unwieldy, much like the call syntax. Then if we have this new of(1, 2, 3)
.let(filter, i => i > 1) // calls func(this, ...) with the given observable.
.let(map, i => i * 10)
.subscribe(x => console.log(x)); Also to @IgorMinar's point we could also introduce a new method on each operator itself (eg of(1, 2, 3)
.let(filter.o(i => i > 1))
.let(map.o(i => i * 10))
.subscribe(x => console.log(x)); The On the TypeScript side there would have to be some explicit typing, as there would be no context to find This also opens the door for something like lodash's var a = _.flow([
filter.o(i => i > 1),
map.o(i => i * 10)
]);
// or
var a = _.flow([
_.partial(filter, _, i => i > 1),
_.partial(map, _, i => i * 10)
]);
a(of(1, 2, 3))
.subscribe(x => console.log(x)); |
source
.let(s => filter.call(s, x => x > 1))
.let(s => map.call(s, x => x + 10)) vs source
.op(filter, x => x > 1)
.op(map, x => x + 10) otherwise someone could do something like const letWrap = (operatorFn) => (...args) => (source) => operatorFn.apply(source, args);
const _filter = letWrap(filter);
const _map = letWrap(map);
source
.let(_filter(x => x > 1))
.let(_map(x => x + 10)) We could even provide the To be clear though, I feel like the solution proposed in this PR is more ergonomic and efficient. |
Another possibility: source.with(filter, map)
.filter(x => x > 1)
.map(x => x + 10) EDIT: Actually, can't always infer names from the function in all runtimes, so it would need to be a hash or something: source.with({ filter, map })
.filter(x => x > 1)
.map(x => x + 10) |
Here is an alternative implementation: #2036 It's based off of my previous comment. |
I tried to get the types flowing for each of these proposals. I got the closest with @Blesh's proposal but it is still not great. The original proposal can be summarized into: interface O<T> {
o(o: (this: O<T>, p: (n: T) => boolean) => O<T>, p: (n: T) => boolean): O<T>;
o<R>(o: (this: O<T>, p: (n: T) => R) => O<R>, p: (n: T) => R): O<R>;
}
function of<T>(...args: T[]): O<T> { return null; }
function map<T, R>(this: O<T>, p: (v: T) => R): O<R> { return null; }
function filter<T>(this: O<T>, p: (v: T) => boolean): O<T> { return null; }
var a = of(1, 2, 3).o(map, n => n.toString())
var b = of(1, 2, 3).o(filter, n => n > 1); The type of The type of The @IgorMinar proposal is better for interface O<T> {
o<R>(t: (s: O<T>) => O<R>): O<R>;
}
function of<T>(...args: T[]): O<T> { return null; }
function map<T, R>(p: (n: T) => R): (s: O<T>) => O<R> { return null; }
function filter<T>(p: (n: T) => boolean): (s: O<T>) => O<T> { return null;}
var a = of(1, 2, 3).o(filter(n => n > 1));
var b = of(1, 2, 3).o(map(n => n.toString())); Here the type of a is The @Blesh proposal is really an interface O<T> {
o<V>(operators: V): O<T> & V;
}
function map<T, R>(this: O<T>, p: (v: T) => R): O<R> { return null; }
function filter<T>(this: O<T>, p: (v: T) => boolean): O<T> { return null; }
function of<T>(...args: T[]): O<T> { return null; }
var a = of(1, 2, 3).o({map}).map(n => n.toString());
var b = of(1, 2, 3).o({filter}).filter(n => n > 1); In this example, the types of all the variables and parameters are what you would expect them to be. var c = of(1, 2, 3).o({filter, map}).filter(n => n > 1).map(n => n.toString()) Here the call to var c = of(1, 2, 3).of({filter}).filter(n => n > 1).o({map}).map(n => n.toString()); |
Talking with @Blesh, he also brought up a pretty neat pattern, using private Symbols. The operators you use internally can be added in a single place like: import { Observable } from 'rxjs/Observable';
import { filter } from 'rxjs/operator/filter';
import { map } from 'rxjs/operator/map';
import { switchMap } from 'rxjs/operator/switchMap';
export const $$filter = Symbol('@@filter');
export const $$map = Symbol('@@map');
export const $$switchMap = Symbol('@@switchMap');
Observable.prototype[$$filter] = filter;
Observable.prototype[$$map] = map;
Observable.prototype[$$switchMap] = switchMap; Then when you want to use them: import { $$map, $$filter } from './operators';
of(1, 2, 3)
[$$filter](i => i > 1)
[$$map](i => i * 10)
.subscribe(x => console.log(x));
// or if you don't like the $$ Symbol prefix convention
of(1, 2, 3)
[filter](i => i > 1)
[map](i => i * 10)
.subscribe(x => console.log(x)); This also is not (currently) typesafe though as described here. |
@jayphelps ... I'm rekindling interest in this, actually. It might be useful in falcor-router to prevent leaking internal implementation details. |
@jayphelps ... how do you feel about just having one method, but naming it |
Oops accidentally closed. |
@Blesh I'm down with import { of } from 'rxjs/observable/of';
import { filter } from 'rxjs/operator/filter';
import { map } from 'rxjs/operator/map';
of(1, 2, 3)
.op(filter, i => i > 1)
.op(map, i => i * 10)
.subscribe(x => console.log(x));
// 20..30 Now that's been a little while I'm curious if @IgorMinar and everyone else has different opinions than before, especially related to lack of type safety. We certainly could require that all operators patch the
I'm not sure I follow? I believe your example would work as-is with this PR, wouldn't it? It doesn't partially apply, it really just proxies (or whatever) |
I started typing that, then accidentally hit "Close and Comment" when I meant to cancel what I was typing. :) It was a stupid idea. |
I think @david-driscoll's idea for adding some type-safety was pretty decent:
Seems viable. Did we after test this? |
Proof of concept using the current playground is available here Code below interface Observable<T> {
op(operator: typeof filter, predicate: (x: T) => boolean): Observable<T>;
}
function filter<T>(o: Observable<T>, predicate: (x: T) => boolean) { }
let o: Observable<string>;
// x is a string here
o.op(filter, x => x === 1);
o.op(filter, x => x === '1');
let o2: Observable<{ a: string; b: number }>;
// x is a string here
o2.op(filter, ({ a, b }) => a === 'hello' && b === 1); |
I'm totally for this feature, but just a quick question: does anybody know if |
@trxcllnt I'm afraid not, since |
@mhegazy ... do you see any compile-time performance problems with @david-driscoll's proposal above? Basically we'd be looking at doing this with 60+ operators, some of which have 6-7 overloads. |
Allows you to apply an operator to a given stream, without it needing to be on the Observable prototype. This is useful whenever you don't want to unintentionally leak implementation details or otherwise rely on the fact that you're using a particular operator. It takes the provided operator function and any arbitrary arguments then internally just calls `operator.apply(this, args)`. This is solves a similar problem as the TC39 proposed [bind operator syntax](https://github.com/tc39/proposal-bind-operator) e.g. `stream::map(i => i + 1)` but that's stage-0 and not supported by TypeScript. Example 1: in your unit tests, if you were to patch additional operators onto the Observable prototype then the application you're testing could unknowingly be relying on those operators without it importing them itself. Your tests would pass because the operator is available in your tests, but when you run the app standalone it errors because the operator wasn't imported! Example 2: you're writing a library that itself uses RxJS. If you patch operators onto the prototype, then consumers of your library could accidentally depend on the fact that the operators you use are available.
Generated by 🚫 dangerJS |
don't believe this PR is applicable anymore |
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Allows you to apply an operator to a given stream, without it needing to be on the Observable prototype. This is mostly useful for library authors where adding operators to
Observable.prototype
can cause headaches to their users.It takes the provided operator function and any arbitrary arguments then internally just calls
operator.apply(this, args)
.This is solves a similar problem as the TC39 proposed bind operator syntax e.g.
stream::map(i => i + 1)
but that's stage-0 and not supported by TypeScript. Note that this current implementation lacks type safety/suggestions, just like it would if you usedmap.call(stream, i => i + 1)
.There is also an alias of just the letter
o
, as in.o(map, i => i + 1)
so your code can remain fairly terse.Without this, you'd need to something like this, which can get rather hairy quick:
The name
operate
and having an short alias likeo
hasn't had a ton of thought into it, just what @Blesh and I came up with in 5 minutes. While I generally hate aliases, the idea was that searching and discoveringo
in our documentation is basically fruitless, but having a long name is annoying when using these often in a library. We considered._(operator)
.$(operator)
.invoke(operator)
Cc/ @IgorMinar ng2 may like this and it adds nearly trivial overhead since it only has the extra function invocation on stream setup, not next()'s so should be out of the hot path for nearly all cases