-
Notifications
You must be signed in to change notification settings - Fork 60
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 26 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,61 @@ | ||
# Eager Inference Annotations for Polymorphic Types | ||
|
||
## Summary | ||
|
||
The RFC introduces a feature to annotate polymorphic function types to express that the first instantiation of a polymorphic type `T` is 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 current solver infers `T` to be of a union type: | ||
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. What is "Luau's current solver?" We have two products right now, Luau's Type Solver and Luau's New Type Solver. "type solver" is really a brand name coined by @andyfriesen, not something external folks necessarily recognize. Could also reasonably describe it as Luau's V2 type inference engine or something of that sort, honestly not entirely sure how we want to deal with the language around the two type inference systems in RFCs. 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. You're right. I considered this while editing the motivation paragraph, but was unsure if "type inference engine V2" was even the correct terminology to use. For now I'll switch the terminology there to "Luau's Type Inference Engine V2"? |
||
|
||
```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 function is intended to constrain the input types to be consistent. | ||
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.
This doesn't mean anything to me. The input type that was inferred in the example is already consistent. Every single instance of the generic 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. Oops. I'll change it to "constrain the subsequent input types to be identical to the first usage." or something, couldn't think of anything better at the time. |
||
|
||
## Design | ||
|
||
We propose adding some symbol as a suffix (or prefix) that annotates the "eager" inference behaviour for a polymorphic type. | ||
Subsequent usages of type `T` where `T` is "eager" would be ignored during instantiation. | ||
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. This description is misleading. They're not ignored during instantiation, you'll still replace each occurrence with the inferred type. The only thing you're proposing changes here, afaik, is that an annotated generic will opt call sites into inferring the exact type of the first argument for automatic instantiation. 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. You're absolutely right, "ignored" is the wrong term. I'll update it to "Subsequent uses of a polymorphic type |
||
|
||
### 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` | ||
``` | ||
|
||
Notably, this behavior would be identical to other languages, such as typescript's `noinfer<T>`. 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. | ||
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 feel like a The current phrasing doesn't mention it as an alternative 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 agree, I'll add an update shortly to add noinfer clearly as an individual alternative. |
||
|
||
|
||
### Keywords | ||
Something like `<greedy T>` or `<strict T>` should also be considered if we want to reduce symbols. This idea has merit when considering the potential complexity of type aliases combined with `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.
This is also a very strong argument against this syntax. 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. Though, is this actually possible under your proposal? You made 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. You're right, 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.
The problem is with inference not syntax as the annotation is defined at the definition, not the argument so there is no conflict in syntax. The problem would be that if a binding is required to be inferred by the first argument that binds to I am curious to know how it worked before with eager inference by default, as that is likely what we want to mirror. |
Uh oh!
There was an error while loading. Please reload this page.