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

Support this pseudo-params in classes #1775

Closed
wants to merge 42 commits into from
Closed

Conversation

popham
Copy link
Contributor

@popham popham commented May 12, 2016

This patch adds support for a this pseudo-param on class methods (static and non-static). With the syntax someMethod(this: this & I,... users can impose requirement beyond "is subtype of the enclosing class," the current behavior. Some goofiness becomes admissible too, e.g. someMethod(this: {})--just don't be goofy (maybe a warning is warranted if the pseudo-param's type is not a subtype of the enclosing class). I have a use case where the value in this extension lies in chaining of static methods, not non-statics, so I've extended class subtyping to structural class statics. I feel like I'm on solid ground after a chat with @jeffmo and @avikchaudhuri on IRC, although during that discussion I forgot to contrast ES5 and Node inheritance against ES6 inheritance. I recall in the past that @samwgoldman and I disagreed on whether class statics should subtype.

Node's inheritance (util.inherit) and the ad hoc inheritance of ES5 (SomeClass.prototype = Object.create(ParentClass.prototype, {...}) without SomeClass.__proto__ = ParentClass) do not chain class statics. Subtyping statics is biased toward ES6 inheritance, whereas the status quo is biased toward Node and ad hoc inheritance. (PR #1847)

Closes #807, #1369, #1704, #1705

A lot of the material here is part of PR #1823. Some has been factored out into PR #1847. Pulls there and a rebase here would remove a lot of noise. The patch is a work-in-progress. There's a few false negatives in the tests. Also, generic class declarations are broken. I want to poll IRC on how I should approach that aspect.

@ghost ghost added the CLA Signed label May 12, 2016
@popham popham force-pushed the pseudo-this branch 2 times, most recently from b83b0d8 to b026c34 Compare May 20, 2016 08:08
@popham popham mentioned this pull request May 20, 2016
10: static absSM(this: Class<this> & Class<V>): string;
^ property `virtualSM` of statics of V. Property not found in
6: declare class X {
^ statics of X
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This loc misses X. Is it even the correct line? See line 36 above for the nonstatic analogue.

@popham popham closed this May 25, 2016
@popham popham reopened this May 25, 2016
@popham popham changed the title Support this pseudo-params in classes, and enforce subtyping on structural class statics Support this pseudo-params in classes May 25, 2016
@popham
Copy link
Contributor Author

popham commented May 26, 2016

I'm incorporating this branch into a project currently, and I have an interface method that I want to bind with return type this, where this is the implementing class. I assumed that I was screwed, but then checked just in case (I recall this being out of bounds on interfaces). It's working for a trivial test, dumb luck would have it. Is there an argument for banishing? Maybe this ought to track interface inheritance chains instead of the implementing class? My short-term bias is obvious here. Adding tests....

method(this: this & I): number {
return this.x + this.anotherMethod();
}
static staticMethod(this: Class<I> & Class<this>): number {
Copy link
Contributor Author

@popham popham May 26, 2016

Choose a reason for hiding this comment

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

Under this: Class<this> & Class<I>, the this-fixing process leaves a tvar flowing to SpeculativeMatchT instead of a concrete type. When the flow finally does trigger, the speculative testing setup has already unwound, and the error leaks.

Swapping the intersection members like they're configured here allows the method to be found on Class<I> without ever touching Class<this>, hence the absence of an error in the current configuration.

Quick and dirty hypothesis check--the following eliminates the spurious error:

    (*Above the (head_l, SpeculativeMatchT) case (calls `speculative_match head_l u`)*)
    | (ClassT instance, UseT (_, SpeculativeMatchT _)) -> (
      let reason = prefix_reason "statics of " (reason_of_t instance) in
      let tvar = mk_tvar cx reason in
      rec_flow cx trace (instance, GetPropT(reason, (reason, "statics"), tvar));
      rec_flow cx trace (tvar, ReposLowerT (reason, u))
    )
    | (InstanceT _, UseT (k, ClassT instance)) ->
      let reason = prefix_reason "statics of " (reason_of_t instance) in
      let tvar = mk_tvar cx reason in
      rec_flow cx trace (instance, GetPropT(reason, (reason, "statics"), tvar));
      rec_flow cx trace (l, UseT(k,tvar))

Find a cleaner fix and remove the TODO.

It looks like concretize_lower_parts and friends is failing, causing this problem.

@popham popham closed this May 26, 2016
@popham popham reopened this May 26, 2016
@avikchaudhuri
Copy link
Contributor

avikchaudhuri commented May 28, 2016

@popham Apologies for not getting the chance to look at this earlier.

My sense is that unless we try doing this feature for all functions (i.e., recognize them as potential open methods that could appear in method bags and be "mixin"d a bunch of times before eventually being called), we'd not know what the entire range of issues are. Internally the type system already carries the type of this for all functions, so it should be possible to lift that up to the surface syntax. This is well-suited for apply/bind/call use cases. But alternative designs may exist. For example, it might turn out to be more advantageous to always restrict the appearance of such methods in bags that satisfy the this type of those methods, etc.

At this point we simply don't know what the general case might look like, and it is an often-enough requested feature that we think we'll be working on it at some point in the near future. Locking down part of the design before we've had the chance to think through the general case seems risky.

Overall the class use-case is simpler, and simpler solutions might exist. We already know a lot about this in classes. The example you mention could easily be handled if we allow declaration of abstract methods (if avoiding the error at definition but throwing the error on call is the main problem you're trying to solve). Relying on an abstract method when defining a method is OK. Calling through a type that only exposes an abstract method is not OK. Done.

I think the general case is much more interesting, and gets to the heart of what's going on with open methods. So I propose we hold off on this PR.

How do you feel about implementing abstract methods (as outlined above) instead? Does that not satisfy all your use cases?

@popham
Copy link
Contributor Author

popham commented May 28, 2016

Fair enough. I expect some OO-dogmatist heads to explode at the idea of an abstract static method. I'll do it initially behind the abstract reserved word.

(With Flow, I have to pause once in a while and remind myself which code makes it through to the run-time build. I figured that having the abstract method syntax aligned with the eventual pseudo-param syntax on functions would make it easier to remember that these "methods" disappear, although the absence of a function body serves as a pretty good reminder. I'll reclassify my fixation on the run-time build as a personal problem :).)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants