# 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.