diff --git a/rfcs/recursive-type-unrestriction.md b/rfcs/recursive-type-unrestriction.md index 9581d7f0..4d9e3681 100644 --- a/rfcs/recursive-type-unrestriction.md +++ b/rfcs/recursive-type-unrestriction.md @@ -45,11 +45,81 @@ Examples like this are quite common in TypeScript code, for example in [ReduxJS] ## Design -*This section to be filled in once we decide which alternative to use* +The design is to continue to use strict type aliases, but to use a +cache during type alias definition. Type aliases are handled as they +currently are, except for recursive cases. + +When defining a type alias `T`, if we encounter a recursive use +`T` we proceed as follows: + +* If every `UI` is `aI`, or if every `UI` does not contain any of the `aJ`s: + * look `` up in the cache, + * if the cache lookup succeeds, return the cached result, + * otherwise create a fresh type variable, add it to the cache, and return it. + +* Otherwise, produce a type error and return an error type. + +Once we are finished defining the type alias, iterate through the cache + +* for each entry with key `` and value `X`, unify `X` with + the result of expanding `T`. + +* expanding `T` may encounter a generic function, + for example `(S) -> R`, which needs a bit of care, since `a` may occur free in + some of the `Ui`. We need to rename `b` to `c`, but this renaming may also + introduce new cache entries, since any cache entry for `U` containing `c` + needs a new cache entry for `U[c/b]`. + +For example, with type + +``` + type TreeWithMap = { data: a, children: {Tree}, map: (a -> b) -> Tree } +``` + +The type alias is `Z` where + +``` + Z = { data: a, children: {Z}, map: (a -> b) -> X } +``` + +the cache contains + +``` + |-> X +``` + +and after unification + +``` + X = { data: b, children: {X}, map: (b -> c) -> Y } +``` + +which introduces a new cache entry + +``` + |-> Y +``` + +and after unification + +``` + Y = { data: b, children: {Y}, map: (c -> b) -> X } +``` + +This algorithm can be generalized to handle mutual recursion, by using a cache for each mutually recursive type. + +This design is the strictest one we came up with that copes with the examples we're interested in, in particular the `PromiseFor` example. ## Drawbacks -*This section to be filled in once we decide which alternative to use* +Renaming can double the size of the type graph, with the result that nested type aliases can result in exponential blowup. + +This algorithm doesn't cope with examples which mix concrete and generic types +``` + type T = { this: a, that; b, children : {T} } +``` + +This algorithm does not support type graphs with infinite expansions. ## Alternatives @@ -82,7 +152,7 @@ recursive types are defined, and used when types are used recursively. For example: ``` -type T = { foo: T? } +type T = { foo: T? } ``` would result in cache entries: ``` @@ -92,7 +162,7 @@ T = { foo: T? } ``` This can result in exponential blowup, for example: ``` -type T = { foo: T?, bar: T? } +type T = { foo: T?, bar: T? } ``` would result in cache entries: ``` @@ -109,12 +179,12 @@ 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>? } +type T = { foo: T>? } ``` results in an infinite type graph with cache: ``` -T = { foo: T>? } -T> = { foo: T>>? } +T = { foo: T>? } +T> = { foo: T>>? } T>> = { foo: T>>>? } ... ``` @@ -128,15 +198,15 @@ We can use occurrence checks to ensure there's less 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>? } +type T = { foo: T>? } ``` since `Promise` is not `a` but contains `a`, and bans ``` -type T = { foo: T? } +type T = { foo: T? } ``` since `a` is not `b`, but allows: ``` -type T = { foo: T? } +type T = { foo: T? } ``` This still has exponential blowup, for example