Skip to content

Commit 327ddca

Browse files
authored
Nested (non-capturing) type aliases (#405)
1 parent 640de95 commit 327ddca

File tree

1 file changed

+253
-0
lines changed

1 file changed

+253
-0
lines changed

proposals/nested-typealias.md

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
# Nested (non-capturing) type aliases
2+
3+
* **Type**: Design proposal
4+
* **Author**: Alejandro Serrano
5+
* **Contributors**: Ivan Kochurkin
6+
* **Discussion**: [KEEP-406](https://github.com/Kotlin/KEEP/issues/406)
7+
* **Status**: Experimental in Kotlin 2.2
8+
* **Related YouTrack issue**: [KT-45285](https://youtrack.jetbrains.com/issue/KT-45285/Support-nested-and-local-type-aliases)
9+
10+
## Abstract
11+
12+
Right now type aliases can only be used at the top level. The goal of this document is to propose a design to allow them within other classifiers, in case they do not capture any type parameters of the enclosing declaration.
13+
14+
## Table of contents
15+
16+
* [Abstract](#abstract)
17+
* [Table of contents](#table-of-contents)
18+
* [Motivation](#motivation)
19+
* [Proposed solution](#proposed-solution)
20+
* [Reflection](#reflection)
21+
* [Multiplatform](#multiplatform)
22+
23+
## Motivation
24+
25+
[Type aliases](https://github.com/Kotlin/KEEP/blob/master/proposals/type-aliases.md) can simplify understanding and maintaining code. For example, we can give a domain-related name to a more "standard" type,
26+
27+
```kotlin
28+
typealias Context = Map<TypeVariable, Type>
29+
```
30+
31+
As opposed to value classes, type aliases are "transparent" to the compiler, so any functionality available through `Map` is also available through `Context`.
32+
33+
Currently, type aliases may only be declared at the top level. This hinders their potential, since type aliases may be very useful in a private part of the implementation; so forcing to introduce the type alias at the top level pollutes the corresponding package. This document aims to rectify this situation, by providing a set of rules for type aliasing within other declarations.
34+
35+
```kotlin
36+
class Dijkstra {
37+
typealias VisitedNodes = Set<Node>
38+
39+
private fun step(visited: VisitedNodes, ...) = ...
40+
}
41+
```
42+
43+
One additional difficulty when type aliases are nested come from the potential **capture** of type parameters from the enclosing type. Consider the following example:
44+
45+
```kotlin
46+
class Graph<Node> {
47+
typealias Path = List<Node> // ⚠️ not supported
48+
}
49+
```
50+
51+
Here the type alias `Path` refers to `Node`, a type parameter of `Graph`. In a similar fashion to variables mentioned within local functions, we say that the `Path` type alias _captures_ the `Node` parameter. In this KEEP we only introduce support for **non-capturing** type aliases. Note that in most cases the captured parameter can be "extracted" as an additional parameter to the type alias itself.
52+
53+
```kotlin
54+
class Graph<Node> {
55+
typealias Path<Node> = List<Node>
56+
}
57+
```
58+
59+
As a consequence of this non-capturing design, type aliases to [inner](https://kotlinlang.org/spec/declarations.html#nested-and-inner-classifiers) classifiers must be restricted.
60+
61+
Going even further than capture, it is a **non-goal** of this KEEP to provide abstraction capabilities over type aliases, like [abstract type members](https://docs.scala-lang.org/tour/abstract-type-members.html) in Scala or [associated type synonyms](https://wiki.haskell.org/GHC/Type_families) in Haskell. Roughly speaking, this would entail declaring a type alias without its right-hand side in an interface or abstract class, and "overriding" it in an implementing class.
62+
63+
```kotlin
64+
interface Collection {
65+
typealias Element
66+
}
67+
68+
interface List<T>: Collection {
69+
typealias Element = T
70+
}
71+
72+
interface IntArray: Collection {
73+
typealias Element = Int
74+
}
75+
```
76+
77+
> [!NOTE]
78+
> This KEEP supersedes the original [type alias KEEP](https://github.com/Kotlin/KEEP/blob/master/proposals/type-aliases.md) on the matter of nested type aliases.
79+
80+
## Proposed solution
81+
82+
We need to care about two separate axes for nested type aliases.
83+
84+
- **Visibility**: we should guarantee that type aliases do not expose types to a broader scope than originally intended.
85+
- **Capturing**: we should guarantee that type parameters of the enclosing type never leak, even when they are implicitly referenced.
86+
87+
As a general _design principle_, nested type aliases should behave similarly to nested classes. This principle also allows freely exchanging classsifiers and type aliases in the source code, a helpful property for refactoring and library evolution.
88+
89+
**Rule 1 (nested type aliases are type aliases)**: nested type aliased must conform to the same [rules of non-nested type aliases](https://github.com/Kotlin/KEEP/blob/master/proposals/type-aliases.md), including rules on well-formedness and recursion.
90+
91+
**Rule 2 (scope)**: nested type aliases live in the same scope as nested classifiers.
92+
93+
- In particular, type aliases cannot be overriden in child classes. Creating a new type alias with the same name as in a parent class merely _hides_ that from the parent.
94+
95+
It is **not** allowed to define local type aliases, that is, to define them in bodies (including functions, properties, initializers, `init` blocks).
96+
97+
**Rule 3 (visibility)**: the visibility of a type alias must be equal to or weaker than the visibility of every type present on its right-hand side. Type parameters mentioned in the right-hand side should not be accounted.
98+
99+
```kotlin
100+
class Service {
101+
internal class Info { }
102+
103+
// wrong: public typealias mentions internal class
104+
typealias One = List<Info>
105+
106+
// ok: private typealias mentions only public and internal classes
107+
private typealias Two = Map<String, Info>
108+
}
109+
```
110+
111+
**Rule 4 (non-capturing)**: nested type aliases may _not_ capture type parameters of the enclosing classifier.
112+
113+
> [!TIP]
114+
> As a rule of thumb, a nested type alias is correct if it could be used as the supertype or a parameter type within a nested class living within the same classifier.
115+
116+
We formally define the set of captured type parameters of a type `A` with enclosing parameters `P`, `capture(A, P)`, as follows.
117+
118+
- If `A` is a type parameter `T`, `capture(T, P) = { T }`;
119+
- If `A` is a nested type access `Outer.Inner`, `capture(Outer.Inner, P) = FromOuter + capture(Inner, FromOuter)` where `FromOuter = capture(Outer, P))`;
120+
- If `A` is an inner type with type arguments `Inner<B, ..., Z>`, `capture(Inner<B, ..., Z>, P) = capture(B, P) + ... + capture(Z, P) + P`;
121+
- If `A` is a non-inner type with type arguments `Class<B, ..., Z>` or a function type `(B, ..., Y) -> Z`, `capture(A, P) = capture(B, P) + ... + capture(Z, P)`;
122+
- If `A` is a nullable type `B?`, `capture(B?, P) = capture(B, P)`;
123+
- If `A` is `*`, then `capture(*, P) = { }`;
124+
- Any other [kinds of types](https://kotlinlang.org/spec/type-system.html#type-kinds) in the Kotlin type system are not denotable, as thus may not appear as the right hand side of a type alias.
125+
126+
For a generic nested type alias declaration,
127+
128+
```kotlin
129+
class Outer<O1, ..., On> {
130+
typealias Alias<T1, ... Tm> = Rhs
131+
}
132+
```
133+
134+
we first compute `capture(Rhs, { O1, .. On })`. The type alias is correct if the result of that computation is a subset of the set of type parameters of the type alias itself, `{ T1, ..., Tm }`.
135+
136+
The following nested type aliases exemplify this calculation, and describe the intuition behind those results.
137+
138+
```kotlin
139+
class Example<T> {
140+
// should be allowed, no type is captured here
141+
typealias Foo = List<Int>
142+
// capture(List<Int>, { T }) = { } ⊆ { } => OK
143+
144+
// should be rejected, since `T` (an argument to the outer `Example`)
145+
// is explicitly mentioned
146+
typealias Bar = List<T>
147+
// capture(List<T>, { T }) = { T } ⊈ { } => not allowed
148+
149+
// should be allowed, since every type parameter (`A`)
150+
// comes from the type alias itself
151+
typealias Baz<A> = List<A>
152+
// capture(List<A>, { T }) = { A } ⊆ { A } => OK
153+
154+
// should be rejected, since `T` is explicitly mentioned
155+
typealias Qux<A> = Map<T, A>
156+
// capture(Map<T, A>, { T }) = { T, A } ⊈ { A } => not allowed
157+
158+
159+
inner class Inner<A> { }
160+
161+
// should be rejected, since we mention `Inner`
162+
// which has an outer `Example` with `T` as type parameter
163+
typealias Moo = Inner<Int>
164+
// capture(Inner<Int>, { T })
165+
// = capture(Int, { T }) + { T }
166+
// = { T } ⊈ { } => not allowed
167+
168+
// should be allowed, since we access `Inner` through
169+
// an explicit `Example<S>` which does not capture `T`
170+
typealias Boo<S> = Example<S>.Inner
171+
// capture(Example<S>.Inner<Int>, { T })
172+
// = capture(Example<S>, { T }) + capture(Inner<Int>, capture(Example<S>, { T }))
173+
// = { S } + capture(Inner<Int>, { S })
174+
// = { S } + capture(Int, { S }) + { S } = { S } ⊆ { S } => OK
175+
176+
}
177+
```
178+
179+
**Rule 5 (type aliases to inner classes)**: whenever a type alias to an inner class, a "type alias constructor" with an extension receiver should be generated, according to the [corresponding specification](https://github.com/Kotlin/KEEP/blob/master/proposals/type-aliases.md#type-alias-constructors-for-inner-classes). This constructor should be generated in the **static** scope for nested type aliases.
180+
181+
```kotlin
182+
// declaration.kt
183+
class A {
184+
inner class B { }
185+
186+
typealias I = B
187+
// generates the following "type alias constructor"
188+
// here "static" is pseudo-syntax only
189+
static fun A.I() = A.B()
190+
}
191+
192+
class C {
193+
typealias D = A.B
194+
// generates the following "type alias constructor"
195+
// here "static" is pseudo-syntax only
196+
static fun A.D() = A.B()
197+
}
198+
199+
// incorrectUsage.kt
200+
val i = A().I() // ⚠️ `I` lives in the static scope of `A`
201+
val d = A().C.D() // ⚠️ cannot use `C.D()` to refer to a function
202+
203+
// correctUsage.kt
204+
import A.* // imports `I`
205+
import C.* // imports `D`
206+
207+
val i = A().I()
208+
val d = A().D()
209+
```
210+
211+
The example above highlights the (maybe surprising) consequence that you cannot use `A().I()` without additional imports, even though those are not required for `A().B()`.
212+
213+
### Reflection
214+
215+
The main reflection capabilities in [`kotlin.reflect`](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.reflect/) work with expanded types. As a result, this KEEP does not affect this part of the library.
216+
217+
The current version of [`kotlinx-metadata`](https://kotlinlang.org/api/kotlinx-metadata-jvm/) already supports [type aliases within any declaration](https://kotlinlang.org/api/kotlinx-metadata-jvm/kotlin-metadata-jvm/kotlin.metadata/-km-declaration-container/type-aliases.html). So in principle the public API is already prepared for this change.
218+
219+
### Multiplatform
220+
221+
Kotlin supports [`expect` and `actual` declarations](https://kotlinlang.org/docs/multiplatform-expect-actual.html) for Multiplatform development.
222+
223+
For top-level declarations, it is forbidden to create a `expect typealias`, but it is allowed to actualize an `expect class` with an `actual typealias`.
224+
225+
We propose to completely forbid nested type aliases to take part on the actualization process. That means that:
226+
227+
- The prohibition about `expect typealias` also covers nested type aliases.
228+
- It is not possible to actualize a nested class with a nested type alias.
229+
230+
Note that this restriction needs to be checked whenever a top-level `expect` class is actualized by a type alias.
231+
232+
```kotlin
233+
// expect.kt
234+
expect class E {
235+
class I
236+
}
237+
238+
// actualIncorrect.kt
239+
class A {
240+
typealias I = Int
241+
}
242+
243+
actual typealias E = A // actualizing nested 'expect class' with typealias not allowed
244+
245+
// actualCorrect.kt
246+
class B {
247+
class I
248+
}
249+
250+
actual typealias E = B // ok
251+
```
252+
253+
Note that in this case actualizing expected nested classes with type aliases allows breaking the assumption that the nested classes actually "lives" within the outer class. In the example above, it may end up being the case that `E.I` (a nested class) is actually `Int`.

0 commit comments

Comments
 (0)