-
Notifications
You must be signed in to change notification settings - Fork 250
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
Update to the Overloads chapter #1839
base: main
Are you sure you want to change the base?
Conversation
* Attempts to clearly define the algorithm for overload matching. * Describes checks for overload consistency, overlapping overloads, and implementation consistency.
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.
(two quick comments)
…overloads * 'overloads' of https://github.com/erictraut/typing: [pre-commit.ci] auto fixes from pre-commit.com hooks # Conflicts: # docs/spec/overload.rst
…overloads * 'overloads' of https://github.com/erictraut/typing: [pre-commit.ci] auto fixes from pre-commit.com hooks
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.
Thanks for working on this tricky area! I haven't finished review yet, but may be called away soon, so I'm submitting the comments I have so far. (EDIT: I've now completed my review.)
We typically wait for a proposed spec change to be accepted by the TC prior to writing conformance tests. In this case, I think it's advisable to write the conformance tests prior to acceptance. This will help us validate the proposed spec changes and tell us if (and to what extent) these changes will be disruptive for existing stubs and current type checker implementations. I would normally volunteer to write the conformance tests, but in this case I think it would be preferable for someone else to write the tests based on their reading of the spec update. If I write the tests, there's a real possibility that they will match what's in my head but not accurately reflect the letter of the spec. There's also a possibility that I'll miss some important cases in the tests. If someone else writes the tests, they can help identify holes and ambiguities in the spec language. Is there anyone willing to volunteer to write a draft set of conformance tests for this overload functionality? I'm thinking that there should be four new test files:
If this is more work than any one person wants to volunteer for, we could split it up. |
I am willing to work on conformance tests for this, but I probably can't get to it until the core dev sprint, Sept 23-27. I realize that implies a delay to moving forward with this PR. Happy for someone else to get to it first. |
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 good to see that this complicated matter is crystallizing into something solid!
After reading through this latest version, a couple of question came to mind:
- What is the type of an overloaded function? When can it be assigned to a
Callable
, and the other way around? - Is a
Protocol
with an overloaded__call__
method an overloaded function type, or does it follow different rules? - When assigning an overloaded function to some
Callable[Tss, R]
, then what happens to the individual signatures? Do they live within theTss
paramspec, and doesR
become causally dependent onTss
, i.e. as a type-mapping? Or does this assignment cause the overloaded type to change into a different function type without overloads? - Do I understand correctly that this spec allows decorating an overloaded function in
.pyi
stubs, as well? Because this is currently not supported in (at least) pyright.
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.
Thanks for specifying overloads in more detail! Overall looks good, just a few comments. This is important for making stubs and third-party libraries behave consistently across type checkers, and without standardization it may be impossible to provide definitions for library functionality that work consistently across type checkers.
Step 5: For each argument, determine whether all possible | ||
:term:`materializations <materialize>` of the argument's type are assignable to | ||
the corresponding parameter type for each of the remaining overloads. If so, | ||
eliminate all of the subsequent remaining overloads. |
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.
This paragraph felt unclear. I had to read it multiple times to understand the intent. Maybe reword this, or include some motivation why we have this rule, so that this can be understood easily without peeking at the following paragraph?
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 think one thing that is unclear about the current wording of this paragraph is that it's not clear to how to interpret "for each argument" in the presence of multiple arguments. Even the example in the next paragraph doesn't really clarify this, since it only discusses one argument. Let's say there are two arguments in the call, and the first argument is as described in the paragraph below. Can we eliminate the third overload from consideration, according to this rule, before we even examine the second argument at all? That's what seems to be implied by the current wording.
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.
Maybe reword this, or include some motivation why we have this rule
Yeah, I agree that it's not very clear. I've taken a stab at adding a motivation and a clearer example.
Can we eliminate the third overload from consideration, according to this rule, before we even examine the second argument at all?
That's a good point. I think the rule needs to be changed from "for each argument" to "for all arguments".
Here's concrete example:
@overload
def func(a: list[Any], b: list[str]) -> str: ...
@overload
def func(a: Any, b: list[Any]) -> float: ...
def test(a: list[Any], b: list[Any]):
func(a, b)
I've updated the rule accordingly. I'll go back and add a more complete conformance test as a separate step.
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.
Thanks, this is now easier to follow.
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.
This is already covered in the typing spec here.
This is already covered in the typing spec here.
This is not currently specified. It's out of scope for this PR.
This is already covered in the typing spec here. The short answer is, no, arbitrary decorators should not be used in stub files. |
Ah ok. The scope of this PR wasn't all that clear to me after reading the PR description. So before I ask anything else; what exactly is within the scope of this PR? |
Ok I wasn't aware of these sections. Maybe it could help to link to them from this spec? |
The scope of this PR is to describe:
These new sections rely upon definitions and concepts described elsewhere in the typing spec including assignability, materialization, type equivalency, enums, tuples, etc. We try hard not to duplicate concept definitions in the spec because duplicate definitions will inevitably get out of sync and cause confusion. This section shouldn't need to say anything about assignability rules for callables or protocols because those are discussed elsewhere in the spec. We've also endeavored to make concepts in the typing spec as orthogonal and composable as possible. If you see cases where concepts are not composing, those are cases that we should discuss. |
How should subtyping overloaded functions behave? I think there is a simple intuition, which is that Example 1: Subtyping between overloaded callback Protocol and Callable type
Example 2: Subclass/sub-protocol consistency
For what it's worth, I think it's reasonable to restrict the union expansion to calls. Union expansion is complicated and has concerning performance implications My understanding is that it is included in the spec because people need it, but if type checkers do not currently implement these rules for subtyping purposes, maybe that's evidence that this need does not extend to subtyping, and we can forgo the rule there entirely? |
I agree that type expansion shouldn't affect (infect) subtyping rules. Assignability rules for overloaded callables are already defined in the spec here. What you're pointing out here is a small inconsistency between call evaluation behavior and the subtyping behavior. This was also recently pointed out by @hauntsaninja in this pyright issue. We could look at amending the assignability rules to eliminate this inconsistency, but this would come at a big price — in terms of complexity and performance. My sense is that it wouldn't be a good tradeoff. I was careful in this PR to talk about type expansion only in the context of "argument types". Arguments are applicable only to calls. |
While looking at overload resolution for Pyre, I noticed an ambiguity in the spec for Step 2. Specifically, what should we do with call argument expressions which have "internal errors"? That is, evaluating the expression leads to an error, but the expression still results in a type. For example A worked example:
Should we specify that overload selection in Step 2 is determined by errors in between the arguments and parameters of the overload signature -- not simply the presence/absence of errors on the call overall? |
When evaluating a call to an overloaded function, I think the behavior for "internal errors" should be the same as with calls to non-overloaded functions. That is, if there are type errors detected when evaluating argument expressions, those errors shouldn't affect the evaluation of the call expression itself. I think that's consistent with what you mean by "errors in between the arguments and parameters". I'm not sure this matters though. Perhaps we can just leave this unspecified. My view is that in cases where a type error is detected and reported by a type checker, any downstream type evaluations that depend on that error are not covered by the spec. Type checkers will generally want to "limit the collateral damage" and reduce downstream false positives once the first error is detected, but I don't think we should try to mandate specific behaviors here. Ultimately, it's up to the user to fix the "inner error" type violation; once that's fixed, then the type checker can guarantee conformant behavior for dependent type evaluations. If you can think of a situation where an "inner error" is detected but not reported to the user during overloaded call evaluation, this would be a bigger concern because it would effectively change the results of the overloaded call without the user realizing it. |
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 spotted some typos and incorrect ReST markup
Co-authored-by: Alex Waygood <[email protected]>
…eement on the proper behavior, and this section is lower priority. We can revisit in the future if there's a desire to do so.
Summary: We know that an overload is constructed from a series of functions, so it has function metadata. Store this directly on the overload. Right now, we just grab the metadata of the first signature, but once python/typing#1839 is accepted, we'll need to check the metadata for consistency between signatures and grab some of it from the overload implementation. Reviewed By: stroxler Differential Revision: D71089700 fbshipit-source-id: 0040a85ee177ade1eaa00cc71f6cbc670f5e2da2
Summary: We know that an overload is constructed from a series of functions, so it has function metadata. Store this directly on the overload. Right now, we just grab the metadata of the first signature, but once python/typing#1839 is accepted, we'll need to check the metadata for consistency between signatures and grab some of it from the overload implementation. Reviewed By: stroxler Differential Revision: D71089700 fbshipit-source-id: 0040a85ee177ade1eaa00cc71f6cbc670f5e2da2
python/typing-council#40