- 
                Notifications
    You must be signed in to change notification settings 
- Fork 66
Add eager inference annotation for polymorphic types #106
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
base: master
Are you sure you want to change the base?
Changes from all commits
213ec9f
              cb82f7a
              bf354a1
              8f1c0d9
              8a24f5b
              9df20a0
              e25643f
              1943be7
              1d36401
              1c8d217
              2ed0187
              b7c2800
              b47e000
              29384af
              f5c5609
              b995140
              5308d81
              c9328c7
              27d8c66
              cbc02bd
              9fa24be
              96f8aaa
              b83c473
              1fee95b
              458dcb6
              8bb92ba
              7d20d52
              2463447
              6264249
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| # Eager Inference Annotations for Polymorphic Types | ||
|  | ||
| ## Summary | ||
|  | ||
| The RFC introduces a feature to annotate polymorphic function types to express that the first bind to `T` will be the one that sticks. | ||
|  | ||
| ## Motivation | ||
|  | ||
| The purpose of this feature is to develop syntax to prevent polymorphic types from widening into (e.g., number | string) when a function is implicitly instantiated with different argument types. E.g., `test(1, "a")`. In the following code, Luau's Type Inference Engine V2 infers `T` to be of a union type: | ||
|  | ||
| ```luau | ||
| function test<T>(a: T, b: T): T | ||
| return a | ||
| end | ||
|  | ||
| local result = test(1, "string") -- inferred type `T`: number | string" | ||
| ``` | ||
|  | ||
| This behaviour can be useful in some cases but is undesirable when a polymorphic type is intended to constrain the subsequent input types to be identical to the first usage. | ||
|  | ||
| ## Design | ||
|  | ||
| We propose adding some symbol as a suffix (or prefix) that annotates the "eager" inference behaviour for a polymorphic type. | ||
| Subsequent uses of a polymorphic type `T` where `T` is "eager" will be inferred as the precise type of the first occurrence. | ||
|  | ||
| ### New Syntax | ||
|  | ||
| The `!` syntax modifier would would enforce an eager inference behaviour for `T!`: | ||
|  | ||
| ```luau | ||
| function test<T!>(a: T, b: T): T | ||
| return a | ||
| end | ||
|  | ||
| test(1, "string") -- TypeError: Expected `number`, got `string` | ||
| ``` | ||
|  | ||
| ## Drawbacks | ||
|  | ||
| - Introduces a new syntax modifier `!`, which may lead to a symbol soup. However, `T!` doesn't seem too shabby next to `T?`. | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 
 I think this is actually a reasonably strong argument against this syntax: people might confuse it as having something to do with optionals or  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @Ukendio There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah might lean onto using keywords or putting the annotation as a prefix instead  | ||
| - Introduces a simple change to luau's parser, marginally increasing parsing complexity. | ||
|  | ||
| ## Alternatives | ||
| ### Per-usage-bindings | ||
| Flip the relationship being declarared per-type-parameter to per-usage which provides more control in expressing the inference, and could allow both instantiation behaviours of polymorphic types under a uniform syntax. | ||
|  | ||
| A polymorphic typed marked with type `T!` will not contribute to the instantiation of type `T` in the function. Instead, `T` should be inferred on the arguments without the annotation: | ||
|  | ||
| ```luau | ||
| function test<T>(first: T, second: T, third: T!): T | ||
| return first | ||
| end | ||
|  | ||
| test(1, "string", true) -- TypeError: Type `boolean` could not be converted into `number | string` | ||
| ``` | ||
|  | ||
| This has the added drawback that the `!` syntax modifier would need to be barred from return types, as the return type holds no relevance to implicit instantiation. | ||
| Also has the major drawback of symbol complexity. E.g., type aliases with `T!?` are entirely possible under this model. | ||
|  | ||
| ### noinfer\<T\> | ||
| Same as above, except we introduce no new syntax, symbol usage, etc. into the language. Create a binding similar or equivalent to typescript's noinfer<T>: | ||
|  | ||
| ```luau | ||
| function test<T>(first: T, second: T, third: noinfer<T>): T | ||
| return first | ||
| end | ||
|  | ||
| test(1, "string", true) -- TypeError: Type `boolean` could not be converted into `number | string` | ||
| ``` | ||
|  | ||
| ### Keywords | ||
| Something like `<greedy T>` or `<strict T>` should also be considered if we want to reduce symbols. | ||
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.
What would be the behavior when a type references
Tmore than once, such as an object?Additionally, what about when the object type is aliased (i.e there's no specific "first occurence")
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 would behave in the same way as the old type solver which is taking the first definition that binds to
T.Similar answer to the last point which is mirroring the old behaviour but in this case would be to go in the order of the fields. I don't know the exact heuristics for that is but I feel most of this could be answered with just saying it should do whatever the type solver did.
I will note that I do think something like
noinfer<T>is a pretty good solution in these cases but I haven't personally prepared to make a case for that proposal.