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

RFC: Type alias type packs #83

Merged
merged 4 commits into from
Oct 27, 2021
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 56 additions & 40 deletions rfcs/syntax-type-alias-type-packs.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Y = X<number, string> -- invalid number of arguments

Additionally, while a simple introduction of these generic type packs into the scope will provide an ability to reference them in function declarations, we want to be able to use them to instantiate other type aliases as well.

Declaration syntax also supports multiple type packs, but we don't have defined semantics on instantiation of such alias.
Declaration syntax also supports multiple type packs, but we don't have defined semantics on instantiation of such type alias.

## Design

Expand All @@ -38,13 +38,15 @@ type X<T...> = --
type A<S...> = X<S...> -- T... = (S...)
```

Similar to function calls, we want to be able to assign multiple regular types to a single type pack:
Similar to function calls, we want to be able to assign zero or more regular types to a single type pack:
```lua
type A = X -- T... = (). Note: check 'Alternatives'
type A = X<> -- T... = ()
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that I would prefer to have this as X<()>.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should we open a straw poll?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I feel like there's only two consistent syntaxes:

  • When specifying type pack arguments, you must specify a type pack explicitly. This means that we don't automatically restructure input sequences, so X<number, string> fails when X accepts one type pack parameter
  • When specifying type pack arguments, and type pack isn't explicitly specified, we take a sequence of types until the last type, or possibly until the next type pack (?), and convert that to a type pack.

In the latter case it feels like X<> should indeed work and produce an empty type pack parameter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I had the first option as an alternative in a draft, but I didn't even want to consider that:

One option that we have is to remove implicit pack assignment from a set of types
and a trailing tail and always require new explicit type pack syntax:

type X<T...> = --

type A<S...> = X<S...>        -- invalid
type B = X                    -- invalid
type C = X<number>            -- invalid
type D = X<number, string>    -- invalid
type E = X<...number>         -- invalid

type A<S...> = X<(S...)>      -- T... = (S...)
type B = X<()>                -- T... = ()
type C = X<(number)>          -- T... = (number)
type D = X<(number, string)>  -- T... = (number, string)
type E = X<(...number)>       -- T... = (...number)

But this doesn't feel natural and for variadic function argument lists, the syntax for
the argument tail is not special.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Types A and E might still work without (), but like I've said it was a draft that I didn't explore.

type B = X<number> -- T... = (number)
type C = X<number, string> -- T... = (number, string)
```

Definition of `A` doesn't parse right now, we would like to make it legal going forward.

Variadic types can also be assigned to type alias type pack:
```lua
type D = X<...number> -- T... = (...number)
Expand All @@ -54,14 +56,20 @@ type E = X<number, ...string> -- T... = (number, ...string)
Multiple regular types can be assigned together with a type pack argument in a tail position:
```lua
type F<S...> = X<number, S...> -- T... = (number, S...)
type G<S...> = X<S..., number> -- error, type arguments can't follow type pack arguments
type G<S...> = X<number, string, S...> -- T... = (number, string, S...)
```

Regular type parameters cannot follow type pack parameters:
```lua
type H<S...> = X<S..., number> -- error, type parameters can't follow type pack parameters
```

### Multiple type pack parameters

We have to keep in mind that it is also possible to declare a type alias that takes multiple type pack parameters.

And since we only allow type pack arguments after regular type arguments, trailing regular types with a type pack that follows are combined into a single type pack argument:
Similar to the previous examples, type parameters that haven't been matched with type arguments are combined together with the next type pack (if present) into the first type pack.
Type pack parameters after the first one have to be type packs:
```lua
type Y<T..., U...> = --

Expand All @@ -82,6 +90,32 @@ type H<S..., R...> = W<number, S..., R...> -- U... = S..., V... = R...
type I<S..., R...> = W<number, string, S..., R...> -- U... = (string, S...), V... = R...
```

### Explicit type pack syntax

To enable additional control for the content of a type pack, especially in cases where multiple type pack parameters are expected, we introduce an explicit type pack syntax for use in type alias instantiation.

Similar to variadic types `...a` and generic type packs `T...`, explicit type packs can only be used at type pack positions:
```lua
type Y<T..., U...> = (T...) -> (U...)

type F1 = Y<(number, string), (boolean)> -- T... = (number, string), U... = (boolean)
type F2 = Y<(), ()> -- T... = (), U... = ()
type F3<S...> = Y<string, S..., (number, S...)> -- T... = (string, S...), U... = (number, S...)
```

In type parameter list, types inside the parentheses always produce a type pack.
vegorov-rbx marked this conversation as resolved.
Show resolved Hide resolved
This is in contrast to function return type pack annotation, where `() -> number` is the same as `() -> (number)`.

This is a breaking change.

Users can already have type alias instantiations like these:
```lua
type X<T> = T?
type A = X<(number)> -- valid right now, typechecking error after this RFC
```

Explicit type pack syntax is not available in other type pack annotation contexts.

## Drawbacks

### Type pack element extraction
Expand Down Expand Up @@ -114,49 +148,31 @@ We wouldn't be able to differentiate if an instantiation results in a type or a

Support for variadic types in the middle of a type pack can be found in TypeScript's tuples.

### Explicit type pack syntax

To enable additional control for the content of a type pack, we would have liked to introduce an explicit type pack syntax.

Similar to variadic types `...a` and generic type packs `T...`, explicit type packs can only be used at type pack positions:
```lua
type Y<T..., U...> = (T...) -> (U...)

type F1 = Y<(number, string), (boolean)> -- T... = (number, string), U... = (boolean)
type F2 = Y<(), ()> -- T... = (), U... = ()
type F3<S...> = Y<string, S..., (number, S...)> -- T... = (string, S...), U... = (number, S...)
```
## Alternatives

This explicit type pack syntax could've been used in other syntax positions where type packs are allowed:
```lua
local function f(...: (number, ...string)) end
local function g(...: (number, (string, number)) end
local function h(): (number, (string, ...boolean)) return 1, 's', true, false end
```
### Backwards compatibility for single type in parentheses
vegorov-rbx marked this conversation as resolved.
Show resolved Hide resolved

Unfortunately, our syntax supports placing parenthesis around the type:
It is possible to allow single element type pack parameter assignment to a type argument:
```lua
local a: number
local b: (number)
type X<T> = T?
type A = X<(number)>
```

This means that the syntax around type packs is ambiguous:
```lua
type Y<T..., U...> = (T...) -> (U...)
This is not proposed to keep separation between type and type packs more clear.
If we supported warning generation, we could create a deprecation period, but since our typechecking errors don't block compilation, it is not that critical.

type A = Y<(number), (number)> -- will resolve as Y<number, number>
### Function return type syntax for explicit type packs

local function f(...: (number)) end -- '...' is a '...number', not a type pack with a single element
```
Another option that was considered is to parse `(T)` as `T`, like we do for return type annotation.

## Alternatives

The syntax we use right now to instantiate type alias with a single type pack parameter to an empty type pack is to not specify the argument list at all:
This option complicates the match ruleset since the typechecker will never know if the user has written `T` or `(T)` so each regular type could be a single element type pack and vice versa.
```lua
type X<T...> = --
type X<T...>
type C = X<number, number> -- T... = (number, number)
type D = X<(number), (number)> -- T... = (number, number)

type A = X -- ok
type B = X<> -- error
```
type Y<T..., U...>

This is the current parser behavior and while we could introduce `X<>` as an alternative, without a warning/deprecation/epoch system we might not be able to disallow the `X` syntax.
--- two items that were enough to satisfy only a single T... in X are enough to satisfy two T..., U... in Y
type E = Y<number, number> -- T... = (number), U... = (number)
```