diff --git a/rfcs/recursive-type-unrestriction.md b/rfcs/recursive-type-unrestriction.md new file mode 100644 index 00000000..dc739c58 --- /dev/null +++ b/rfcs/recursive-type-unrestriction.md @@ -0,0 +1,121 @@ +# Loosening the recursive type restriction + +## Summary + +Luau supports recursive type aliases, but with an important +restriction: users can declare functions of recursive types, but *not* +recursive type functions. This has problems with sophisticated uses of +types, for example ones which mix recursive types and nested generics. +This RFC proposes loosening this restriction. + +## Motivation + +Luau supports recursive type aliases, but with an important restriction: +users can declare functions of recursive types, such as: +```lua + type Tree = { data: a, children: {Tree} } +``` +but *not* recursive type functions, such as: +```lua + type TreeWithMap = { ..., map: (a -> b) -> Tree } + +``` +These examples cope up naturally in OO code bases with generic types. + +## Design + +*This section to be filled in once we decide which alternative to use* + +## Drawbacks + +*This section to be filled in once we decide which alternative to use* + +## Alternatives + +### Lazy recursive type instantiations + +The most permissive change would be to make recursive type +instantiation lazy rather than strict. In this approach `T` would +not be instantiated immediately, but only when the body is needed. In +particular, during unification we can unify `T` with `T` by +first trying to unify `U` and `V`, and only if that fails try to unify +the instantiations. + +*Advantages*: this allows recursive types with infinite expansions like: +```lua + type Foo = { ..., promises: {Foo>} } +``` + +### Lazy recursive type instantiations with a cache + +As above, but keep a cache for each type function. + +*Advantages*: reduces the size of the type graph. + +### Strict recursive type instantiations with a cache + +Rather than lazily instantiating type functions when they are used, we +could carry on instantiating them when they are defined, and use a +cache to reuse them. In particular, the cache would be populated when the +recursive types are defined, and used when types are used recursively. + +For example: +``` +type T = { foo: T? } +``` +would result in cache entries: +``` +T = { foo: T? } +T = { foo: T? } +T = { foo: T? } +``` +This can result in exponential blowup, for example: +``` +type T = { foo: T?, bar: T? } +``` +would result in cache entries: +``` +T = { foo: T?, bar: T? } +T = { foo: T?, bar: T? } +T = { foo: T?, bar: T? } +T = { foo: T?, bar: T? } +T = { foo: T?, bar: T? } +T = { foo: T?, bar: T? } +T = { foo: T?, bar: T? } +``` +Applying this to a type function with N type variables results in more than 2^N +types. Because of blowup, we would need a bound on cache size. + +This can also result in the cache being exhausted, for example: +``` +type T = { foo: T>? } +``` +results in an infinite type graph with cache: +``` +T = { foo: T>? } +T> = { foo: T>>? } +T>> = { foo: T>>>? } +... +``` + +*Advantages*: types are computed strictly, so we don't have to worry about lazy types +producing unbounded type graphs during unification. + +### Strict recursive type instantiations with a cache and an occurrence check + +We can use occurrence checks to ensure there's no blowup. We can restrict +a recursive use `T` in the definition of `T` so that either `UI` is `aI` +or contains none of `a1...aN`. For example this bans +``` +type T = { foo: T>? } +``` +since `Promise` is not `a` but contains `a`, and bans +``` +type T = { foo: T? } +``` +since `a` is not `b`, but allows: +``` +type T = { foo: T? } +``` + +*Advantages*: types are computed strictly, and may produce better error messages if the occurs check fails.