-
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
Typing fixes #1214
Typing fixes #1214
Conversation
@@ -1,7 +1,7 @@ | |||
import {Subscriber} from './Subscriber'; | |||
|
|||
export class Operator<T, R> { | |||
call<T, R>(subscriber: Subscriber<R>): Subscriber<T> { | |||
call<R>(subscriber: Subscriber<R>): Subscriber<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, the type R
of call
does not have any relation with R
of Operator
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch; that's incorrect. R
should be bound to the type's R
as well. I think fixing this will greatly expand the scope of operators that need to be touched however. I'll try it out.
Just minor details regarding the commit messages: you should not capitalize the first letter, and you should use present tense. More info on CONTRIBUTING.md#subject |
@luisgabriel Sorry, missed that bit in the contribution guidelines. Looks like fixing that second parameter has indeed broken a bunch of other operators that can only be fixed by introducing I've actually already worked out the latter approach in a different branch (I'm planning on retracing the changes in small commits), but it involves lots of code being touched throughout the codebase. Would it be better for now to |
I'm not sure what would be the best solution here. Could you point out this branch you said? I think I can have a better idea of the problem size. It's also worth to wait for some inputs from @kwonoj and @david-driscoll in this matter. |
Sure; take a look at the You could probably get a better idea of which operators would need to be fixed solely as part of this bugfix by:
|
I was trying same (get rid of My vote in here is make changes correctly at once, avoiding one additional steps of introducing |
I agree that would be the ideal approach @kwonoj, but the problem is that making the types flow correctly inside the implementations requires a lot of domain knowledge and isn't a simple chore type task. For this reason it would be easier if it could be fixed in separate PRs on a case by case basis. In the |
As I already mentioned, I agree that requires sufficient time to make corresponding changes. I'm not object to make separate PR for those effort, while this PR leaving |
Ok, I see your point. :) So in terms of direction forward, here's a couple of problems that pop up after removing the
There are also compile errors in the following files, however I already have tentative fixes for each of these in
I will clean these up and add them as individual commits to this branch. Once all of these are addressed I think we will be rid of the extra |
@kwonoj Ok, so I've cleaned up all the ones I know how to fix.The only remaining failures at this point are the |
Wow, this is huge ;) appreciate for effort. Let's start look into, I'll add some inputs if I have some questions around. /cc @david-driscoll also if he could give some helps. |
export function pairwise<T>(): Observable<T> { | ||
return this.lift(new PairwiseOperator()); | ||
export function pairwise<T>(): Observable<[T, T]> { | ||
let _this: Observable<T> = this; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why _this
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+whereever possible, let's use const
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@luisgabriel The idea is that using this
gives you any
, and then all type checking from that point on is disabled. Using only _this
ensures your type signatures are compatible (i.e. lifting through the operator actually produces the output type of the function). We can also do this using casts, but I arrived at this pattern over the course of a number of changes across other operators, and repeatedly casting to Observable<T>
becomes tedious (and of course if you forget, you don't get any errors from the compiler).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. Thanks for the explanation.
I agree with you. This pattern leads to a better readability over casting. I'd prefer to name it self
instead of _this
, but this is a minor detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would go with self
probably because the compiler will user _this
internally for fat arrow methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm bit on fence between casting vs using explicit typed reference. So it'll be basically
const self: Observable<T> = this;
self.doxxx();
vs.
((Observable<T>)this).doXXX()...
correct? While I like prior snippet's clarity of type to self
, also makes feel does it necessarily have to hold another reference which is repeated over every operator. (course I also does agree @masaeedu about casting, just speaking out loud to hear some opinions)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kwonoj That's correct. David raised the performance aspect of this as well. Just so we can make this explicit, the concern is that the instance of Observable
would be disposed by the GC if not for the _this
/self
reference, so adding this alias will result in memory pressure/a full out leak. Is this a fair assessment?
I will try to set up a test in Chrome memory mode to see how the alias affects frequency of GC and memory usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I was a betting man, my bet would be that this has no significant impact, since the locally scoped self
reference is orphaned as soon as the operator function returns (provided we don't start passing it into closures). So the lifetime of the object is dictated only by how long external references to the observable persist.
However, as mentioned earlier, I'll do a test in Chrome to backup my handwaving :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing this properly is blocked by #1228, so I'll just change these back to this
for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
on the self
vs _this
vs source
...
So far source
has been an idiom in this library. While I realize that these all end up as members of a class, the reality is that it's only so we can compose the operators left to right. These operators rely on almost no state at all from the instance, and under the right conditions (say the passage of the "pipe operator" |>
), they could just be functions.
Let's stick with source
. There isn't a compelling reason to change it.
The point about I'm inclined to say cast it to |
@@ -1,7 +1,11 @@ | |||
import {Subscriber} from './Subscriber'; | |||
|
|||
export class Operator<T, R> { | |||
call<T, R>(subscriber: Subscriber<R>): Subscriber<T> { | |||
export interface Operator<T, R> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we expect export interface and implementation both under same name? would you able to elaborate use case for interface is required with same name of implementation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kwonoj I think this was as a result of some difference in parameter variance when implementing an interface instead of "implementing" a class. I can't recall exactly why I had this, so will remove.
@kwonoj Would you agree just casting to |
: It'd be appreciate if you can point 'here', since comment is not pointing specific code and due to size of change, it's bit hard to find right context. ;) |
Sorry, I was referring to David's comment there, should have made it clearer. If you look at my last commit, you'll see the problem. The |
@david-driscoll, @kwonoj Would it make sense to just remove the E.g. you have a Right now the code is trying to produce a new
Removing the override of |
As an immediate thought I think it's fine to creating chain of subject via lift, but interesting point. Interface already limits to return observable only, limiting created object to behave as subject.. @masaeedu What about creating those as separate issue? I don't think this PR's scope can contain those changes anyway. (also this PR's already has bit of size for its own discussion) |
@kwonoj The reason I'm bringing this up here is because we need to make |
I think we can choose between
I'll leave up to you which way to proceed. |
Ok, opened #1234 and will nuke the |
|
||
let _this: Observable<T> = this; | ||
if (_this._isScalar) { | ||
let scalar: ScalarObservable<T> = <any>_this; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this transpiles, won't it have two additional lines where we're setting _this = this
and then scalar = _this
? I'm all for good typing for the exposed API. Our internals, though, I just care that they work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's true that there's no direct benefit to end users for this kind of change, but since I was doing typing fixes in this PR, I also tried to prevent mistakes in typings in the future. The isEmpty
operator you commented on earlier is a good example of this. You're trying to take an Observable<T>
and produce an Observable<boolean>
, but you're using an Operator<boolean, boolean>
to do it, which the type system would disqualify. If you cast this
to (<Observable<T>>this)
, the compiler complains about how the operator has the wrong type, which has the knock on effect of forcing you to fix your types all the way down to IsEmptySubscriber
.
In and of itself this has no effect on the end user, but in many cases the user facing type signature is wrong simply because of a break in type checking somewhere down the line. It's also true that things "just work" because the people that have been contributing so far understand the codebase well, and you have good unit tests to cover your work. In some cases people might not have as good an understanding of the overall codebase (yours truly being a good example), and its nice to have accurate static typing to hold your hand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll also point out that the extra emits have an effect proportional to the number of calls to every
in the program, not the number of items that pass through the every
operator. A large sized program will have, let's say a few thousand invocations (generously) of the operator functions, so an extra local variable per shouldn't have any performance impact.
All this aside, I'll be removing the _this
aliases for this PR and will open a separate issue to discuss the pros/cons of it.
@@ -13,14 +13,15 @@ import {errorObject} from '../util/errorObject'; | |||
* @returns {Obervable} An observable of the accumulated values. | |||
*/ | |||
export function scan<T, R>(accumulator: (acc: R, x: T) => R, seed?: T | R): Observable<R> { | |||
return this.lift(new ScanOperator(accumulator, seed)); | |||
let _this: Observable<T> = this; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, for this sort of thing, I fail to see the benefit to our end users. It's just going to add to the transpiled output, I believe.
@masaeedu can you rebase this PR please? Also: just a note on PRs like this in general: If it's internal code typings your changing (read: Nothing that a developer-user would interface with)... My feeling is that |
Would love to see this one in fairly soon, working on additional operator signatures at the moment and ran into an issue were the changes ran into these same issues. @masaeedu 👍 |
Sorry about the delay folks. I'll be cleaning this up and rebasing on Saturday. |
@Blesh Ok, rebased and removed most of the changes since they were controversial. The changes now are limited to removing redundant generics from This still leaves a number of operators where the internal typings (and in some cases user-facing typings) are wrong, but they still compile since cc @kwonoj |
@masaeedu maybe it would be worth to create an issue listing the operators with wrong typing (especially the external ones) so we could try to fix it incrementally. |
Sorry for delays, I'll start look into updated changes. |
Change looks good to me, nice PR @masaeedu 😄 |
I'll check in this PR after leaving bit more to see if additional suggestions around, expecting later today to tomorrow morning. cc @david-driscoll @luisgabriel @Blesh for visibility. |
LGTM. Thanks @masaeedu for this nice PR. It raised important discussions regarding the typings. |
LGTM 👍 |
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. |
The
Observable.prototype.lift
andOperator.prototype.call
have unbound generic type parameters at the function level. Current signatures are:The
T
type parameter in each of those functions shouldn't be there, sinceT
in each of those definitions needs to be bound to theT
argument of the containing type.The other changes are removing the redundant type argument in a few operators. The change also revealed that
MergeMapToOperator
doesn't implement theOperator
interface correctly:An
Operator<Observable<T>, R2>
needs to implement acall
method that returnsSubscriber<Observable<T>>
, notSubscriber<T>
. Since I'm not familiar with what themergeMapTo
operator is supposed to do (the doc comment is missing), I couldn't changeMergeMapToSubscriber
to implementSubscriber<Observable<T>>
. I've fixed it by replacingT
withany
for now, but this may bear further investigation.