From 9e4885bc1d5d63d2366d156e4d74dde0ed2fdbfc Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 17 Jul 2024 10:41:11 +0200 Subject: [PATCH 01/17] Nested typealias --- proposals/nested-typealias.md | 118 ++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 proposals/nested-typealias.md diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md new file mode 100644 index 000000000..4f788fd15 --- /dev/null +++ b/proposals/nested-typealias.md @@ -0,0 +1,118 @@ +# Nested (and inner) type aliases + +* **Type**: Design proposal +* **Author**: Alejandro Serrano +* **Contributors**: +* **Discussion**: +* **Status**: +* **Related YouTrack issue**: [KT-45285](https://youtrack.jetbrains.com/issue/KT-45285/Support-nested-and-local-type-aliases) + +## Abstract + +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. + +## Table of contents + +* [Abstract](#abstract) +* [Table of contents](#table-of-contents) +* [Motivation](#motivation) +* [Proposed solution](#proposed-solution) +* [Design questions](#design-questions) + +## Motivation + +Type aliases can simplify understanding and maintaining code. For example, we can give a domain-related name to a more "standard" type, + +```kotlin +typealias Context = Map +``` + +As opposed to value classes, type aliases are "transparent" to the compiler, so any functionality available through `Map` is also available through `Context`. + +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. + +```kotlin +class Dijkstra { + typelias VisitedNodes = Set + + private fun step(visited: VisitedNodes, ...) = ... +} +``` + +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 left-hand side in an interface or abstract class, and "overriding" it in an implementing class. + +```kotlin +interface Collection { + typealias Element +} + +interface List: Collection { + typealias Element = T +} + +interface IntArray: Collection { + typealias Element = Int +} +``` + +## Proposed solution + +We need to care about two separate axes for nested type aliases. + +- **Visibility**: we should guarantee that type aliases do not expose types to a broader scope than originally intended. +- **Inner**: classifiers defined inside another classifier may be marked as [inner](https://kotlinlang.org/spec/declarations.html#nested-and-inner-classifiers), which requires an instance of the outer type to be in the context. Once again, nested type aliases should not break those guarantees. + +We extend the syntax of type aliases as follows. A type alias declaration marked with the `inner` keyword is said to be _inner_, otherwise we refer to it as _nested_. + +``` +[modifiers] ['inner'] 'typealias' simpleIdentifier [{typeParameters}] '=' type +``` + +**Rule 1 (visibility)**: the visibility of a type alias must be equal to or weaker than the visibility of every type present in its left-hand side. + +```kotlin +class Outer { + internal class Inner { } + + // wrong: public typealias mentions internal class + typealias One = List + + // ok: private typealias mentions only public and internal classes + private typealias Two = Map +} +``` + +**Rule 2 (inner)**: type aliases referring to inner classes must be marked as `inner`. + +- Inner type aliases may also refer only to non-inner classes. + +```kotlin +class Big { + class Nested { } + inner class Inner { } + + typealias A = Nested // ok + typealias B = Inner // wrong + inner typealias C = Nested // ok + inner typealias D = Inner // ok +} +``` + +## Design questions + +**Question 1 (inner on other values)**: should we allow inner type aliases to depend not only on the outer instance but also on other properties? + +```kotlin +class Foo(val big: Big) { // 'Big' as above + inner typealias X = big.Inner +} +``` + +One important question here is what is the semantics of such a definition: + +- Is the left-hand side chosen during initialization? +- Should we ensure that the type alias is somehow "stable"? If so, what are the rules for such stable references to types? + +**Question 2 (expect / actual)**: should we allow nested `expect` (and correspondingly, `actual`) type aliases? + +- This is very close to abstracting over types, which is a non-goal of this KEEP. \ No newline at end of file From 22531668bd96ed4ac4698122228d54b53064bfbd Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 17 Jul 2024 10:46:07 +0200 Subject: [PATCH 02/17] Type parameters --- proposals/nested-typealias.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index 4f788fd15..ad743729d 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -68,7 +68,7 @@ We extend the syntax of type aliases as follows. A type alias declaration marked [modifiers] ['inner'] 'typealias' simpleIdentifier [{typeParameters}] '=' type ``` -**Rule 1 (visibility)**: the visibility of a type alias must be equal to or weaker than the visibility of every type present in its left-hand side. +**Rule 1 (visibility)**: the visibility of a type alias must be equal to or weaker than the visibility of every type present on its left-hand side. ```kotlin class Outer { @@ -82,7 +82,16 @@ class Outer { } ``` -**Rule 2 (inner)**: type aliases referring to inner classes must be marked as `inner`. +**Rule 2 (type parameters)**: nested type aliases may refer to the type parameters of the enclosing classifier. They may also add their own, as usual with type aliases. + +```kotlin +class Example { + typealias Bar = List + typealias Quux = Map +} +``` + +**Rule 3 (inner)**: type aliases referring to inner classes must be marked as `inner`. - Inner type aliases may also refer only to non-inner classes. From 7a6541f8f12bbd294af67b73619dece5094a6049 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 17 Jul 2024 10:54:26 +0200 Subject: [PATCH 03/17] Scope --- proposals/nested-typealias.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index ad743729d..fe63d5e20 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -68,7 +68,11 @@ We extend the syntax of type aliases as follows. A type alias declaration marked [modifiers] ['inner'] 'typealias' simpleIdentifier [{typeParameters}] '=' type ``` -**Rule 1 (visibility)**: the visibility of a type alias must be equal to or weaker than the visibility of every type present on its left-hand side. +**Rule 1 (scope)**: nested type aliases live in the same scope as nested classifiers and inner type aliases live in the same scope as inner classifiers. + +- 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_ the parent one. + +**Rule 2 (visibility)**: the visibility of a type alias must be equal to or weaker than the visibility of every type present on its left-hand side. ```kotlin class Outer { @@ -82,7 +86,7 @@ class Outer { } ``` -**Rule 2 (type parameters)**: nested type aliases may refer to the type parameters of the enclosing classifier. They may also add their own, as usual with type aliases. +**Rule 3 (type parameters)**: nested type aliases may refer to the type parameters of the enclosing classifier. They may also add their own, as usual with type aliases. ```kotlin class Example { @@ -91,7 +95,7 @@ class Example { } ``` -**Rule 3 (inner)**: type aliases referring to inner classes must be marked as `inner`. +**Rule 4 (inner)**: type aliases referring to inner classes must be marked as `inner`. The same [restrictions to inner classes](https://kotlinlang.org/spec/declarations.html#nested-and-inner-classifiers) apply to inner type aliases. - Inner type aliases may also refer only to non-inner classes. From be6b5e64779c291c227568bc20a264c4d013100a Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 17 Jul 2024 11:42:30 +0200 Subject: [PATCH 04/17] Reflection --- proposals/nested-typealias.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index fe63d5e20..76830ba78 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -17,6 +17,7 @@ Right now type aliases can only be used at the top level. The goal of this docum * [Table of contents](#table-of-contents) * [Motivation](#motivation) * [Proposed solution](#proposed-solution) + * [Reflection](#reflection) * [Design questions](#design-questions) ## Motivation @@ -111,6 +112,12 @@ class Big { } ``` +### Reflection + +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. + +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. + ## Design questions **Question 1 (inner on other values)**: should we allow inner type aliases to depend not only on the outer instance but also on other properties? From 6e2124859e412133aed4ecc666b79120c12f80b3 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 23 Oct 2024 09:50:47 +0200 Subject: [PATCH 05/17] !fixup --- proposals/nested-typealias.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index 76830ba78..6748fa091 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -40,7 +40,7 @@ class Dijkstra { } ``` -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 left-hand side in an interface or abstract class, and "overriding" it in an implementing class. +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. ```kotlin interface Collection { @@ -73,7 +73,7 @@ We extend the syntax of type aliases as follows. A type alias declaration marked - 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_ the parent one. -**Rule 2 (visibility)**: the visibility of a type alias must be equal to or weaker than the visibility of every type present on its left-hand side. +**Rule 2 (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. ```kotlin class Outer { @@ -130,7 +130,7 @@ class Foo(val big: Big) { // 'Big' as above One important question here is what is the semantics of such a definition: -- Is the left-hand side chosen during initialization? +- Is the type side chosen during initialization? - Should we ensure that the type alias is somehow "stable"? If so, what are the rules for such stable references to types? **Question 2 (expect / actual)**: should we allow nested `expect` (and correspondingly, `actual`) type aliases? From ce06ad51c6a1a78796ee19084c5653b8437dd445 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 31 Oct 2024 09:51:02 +0100 Subject: [PATCH 06/17] Improvements on inner type aliases --- proposals/nested-typealias.md | 66 ++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index 6748fa091..5dd009210 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -61,7 +61,7 @@ interface IntArray: Collection { We need to care about two separate axes for nested type aliases. - **Visibility**: we should guarantee that type aliases do not expose types to a broader scope than originally intended. -- **Inner**: classifiers defined inside another classifier may be marked as [inner](https://kotlinlang.org/spec/declarations.html#nested-and-inner-classifiers), which requires an instance of the outer type to be in the context. Once again, nested type aliases should not break those guarantees. +- **Inner**: classifiers defined inside another classifier may be marked as [inner](https://kotlinlang.org/spec/declarations.html#nested-and-inner-classifiers), which means they capture the type parameters of the enclosing type. Once again, nested type aliases should not break those guarantees. We extend the syntax of type aliases as follows. A type alias declaration marked with the `inner` keyword is said to be _inner_, otherwise we refer to it as _nested_. @@ -73,7 +73,7 @@ We extend the syntax of type aliases as follows. A type alias declaration marked - 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_ the parent one. -**Rule 2 (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. +**Rule 2 (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. ```kotlin class Outer { @@ -87,29 +87,50 @@ class Outer { } ``` -**Rule 3 (type parameters)**: nested type aliases may refer to the type parameters of the enclosing classifier. They may also add their own, as usual with type aliases. +**Rule 3 (type parameters)**: nested type aliases may _not_ refer to the type parameters of the enclosing classifier. Inner type aliases may do. In both cases type parameters may be included as part of the type alias declaration itself. ```kotlin class Example { - typealias Bar = List - typealias Quux = Map + // this alias is not allowed to capture `T` + typealias Foo = List + + // these aliases capture the type parameter `T` + inner typealias Bar = List + inner typealias Qux = Map + + // inner type aliases may, but need not capture + inner typealias Moo = Int } ``` -**Rule 4 (inner)**: type aliases referring to inner classes must be marked as `inner`. The same [restrictions to inner classes](https://kotlinlang.org/spec/declarations.html#nested-and-inner-classifiers) apply to inner type aliases. +We refer to nested and inner typealiases in the same way that we do with nested and inner classifiers. + +```kotlin +fun example1( + foo: Example.Foo, + bar: Example.Bar, + qux: Example.Qux, + moo: Example.Moo, +) +``` + +**Rule 4 (inner classes)**: type aliases which refer to inner classes within the same classifier and do not prefix it with a path must be marked as `inner`. The reasoning behind it is that inner classes implicitly capture the type parameters of the outer class. -- Inner type aliases may also refer only to non-inner classes. +- Note that it is possible to create a nested type alias when a full type is given. ```kotlin -class Big { - class Nested { } - inner class Inner { } - - typealias A = Nested // ok - typealias B = Inner // wrong - inner typealias C = Nested // ok - inner typealias D = Inner // ok +class Example { + inner class Inner + + inner typealias One = Inner + typealias Two = Example.Inner } + +fun example2( + inner: Example.Inner, + one: Example.One, + two: Example.Two, // expands to Example.Inner +) ``` ### Reflection @@ -120,19 +141,6 @@ The current version of [`kotlinx-metadata`](https://kotlinlang.org/api/kotlinx-m ## Design questions -**Question 1 (inner on other values)**: should we allow inner type aliases to depend not only on the outer instance but also on other properties? - -```kotlin -class Foo(val big: Big) { // 'Big' as above - inner typealias X = big.Inner -} -``` - -One important question here is what is the semantics of such a definition: - -- Is the type side chosen during initialization? -- Should we ensure that the type alias is somehow "stable"? If so, what are the rules for such stable references to types? - -**Question 2 (expect / actual)**: should we allow nested `expect` (and correspondingly, `actual`) type aliases? +**Expect / actual**: should we allow nested `expect` (and correspondingly, `actual`) type aliases? - This is very close to abstracting over types, which is a non-goal of this KEEP. \ No newline at end of file From 19d5f1c84055014f75197d6c175c5171e82de6f2 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 1 Nov 2024 09:42:14 +0100 Subject: [PATCH 07/17] Clarifications --- proposals/nested-typealias.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index 5dd009210..1f6159846 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -61,7 +61,7 @@ interface IntArray: Collection { We need to care about two separate axes for nested type aliases. - **Visibility**: we should guarantee that type aliases do not expose types to a broader scope than originally intended. -- **Inner**: classifiers defined inside another classifier may be marked as [inner](https://kotlinlang.org/spec/declarations.html#nested-and-inner-classifiers), which means they capture the type parameters of the enclosing type. Once again, nested type aliases should not break those guarantees. +- **Inner**: classifiers defined inside another classifier may be marked as [inner](https://kotlinlang.org/spec/declarations.html#nested-and-inner-classifiers), which means they capture the type parameters of the enclosing type. Type aliases should not break any type system guarantees. We extend the syntax of type aliases as follows. A type alias declaration marked with the `inner` keyword is said to be _inner_, otherwise we refer to it as _nested_. @@ -71,19 +71,19 @@ We extend the syntax of type aliases as follows. A type alias declaration marked **Rule 1 (scope)**: nested type aliases live in the same scope as nested classifiers and inner type aliases live in the same scope as inner classifiers. -- 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_ the parent one. +- 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. **Rule 2 (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. ```kotlin -class Outer { - internal class Inner { } +class Service { + internal class Info { } // wrong: public typealias mentions internal class - typealias One = List + typealias One = List // ok: private typealias mentions only public and internal classes - private typealias Two = Map + private typealias Two = Map } ``` @@ -103,7 +103,7 @@ class Example { } ``` -We refer to nested and inner typealiases in the same way that we do with nested and inner classifiers. +We refer to nested and inner typealiases in the same way that we do with other nested and inner classifiers. ```kotlin fun example1( @@ -116,7 +116,7 @@ fun example1( **Rule 4 (inner classes)**: type aliases which refer to inner classes within the same classifier and do not prefix it with a path must be marked as `inner`. The reasoning behind it is that inner classes implicitly capture the type parameters of the outer class. -- Note that it is possible to create a nested type alias when a full type is given. +- Note that it is possible to create a nested (not inner!) type alias when a full type is given. ```kotlin class Example { From f487a3ea635fc743b96e77bd9f412fec9c3332d3 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 1 Nov 2024 09:43:27 +0100 Subject: [PATCH 08/17] Update contributors --- proposals/nested-typealias.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index 1f6159846..977deedd1 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -2,7 +2,7 @@ * **Type**: Design proposal * **Author**: Alejandro Serrano -* **Contributors**: +* **Contributors**: Ivan Kochurkin * **Discussion**: * **Status**: * **Related YouTrack issue**: [KT-45285](https://youtrack.jetbrains.com/issue/KT-45285/Support-nested-and-local-type-aliases) From 8ca913a5b2b8675bbabae83fd229bb3238a1b02e Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 18 Dec 2024 12:28:42 +0100 Subject: [PATCH 09/17] Rework KEEP --- proposals/nested-typealias.md | 157 ++++++++++++++++++++++++---------- 1 file changed, 111 insertions(+), 46 deletions(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index 977deedd1..cfe6ccc2c 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -1,4 +1,4 @@ -# Nested (and inner) type aliases +# Nested (non-capturing) type aliases * **Type**: Design proposal * **Author**: Alejandro Serrano @@ -9,7 +9,7 @@ ## Abstract -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. +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. ## Table of contents @@ -18,11 +18,11 @@ Right now type aliases can only be used at the top level. The goal of this docum * [Motivation](#motivation) * [Proposed solution](#proposed-solution) * [Reflection](#reflection) -* [Design questions](#design-questions) + * [Multiplatform](#multiplatform) ## Motivation -Type aliases can simplify understanding and maintaining code. For example, we can give a domain-related name to a more "standard" type, +[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, ```kotlin typealias Context = Map @@ -40,7 +40,25 @@ class Dijkstra { } ``` -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. +One additional difficulty when type aliases are nested come from the potential **capture** of type parameters from the enclosing type. Consider the following example: + +```kotlin +class Graph { + typealias Path = List // ⚠️ not supported +} +``` + +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. + +```kotlin +class Graph { + typealias Path = List +} +``` + +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. + +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. ```kotlin interface Collection { @@ -56,24 +74,27 @@ interface IntArray: Collection { } ``` +> [!NOTE] +> 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. + ## Proposed solution We need to care about two separate axes for nested type aliases. - **Visibility**: we should guarantee that type aliases do not expose types to a broader scope than originally intended. -- **Inner**: classifiers defined inside another classifier may be marked as [inner](https://kotlinlang.org/spec/declarations.html#nested-and-inner-classifiers), which means they capture the type parameters of the enclosing type. Type aliases should not break any type system guarantees. +- **Capturing**: we should guarantee that type parameters of the enclosing type never leak, even when they are implicitly referenced. -We extend the syntax of type aliases as follows. A type alias declaration marked with the `inner` keyword is said to be _inner_, otherwise we refer to it as _nested_. +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. -``` -[modifiers] ['inner'] 'typealias' simpleIdentifier [{typeParameters}] '=' type -``` +**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. -**Rule 1 (scope)**: nested type aliases live in the same scope as nested classifiers and inner type aliases live in the same scope as inner classifiers. +**Rule 2 (scope)**: nested type aliases live in the same scope as nested classifiers. - 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. -**Rule 2 (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. +It is **not** allowed to define local type aliases, that is, to define them in bodies (including functions, properties, initializers, `init` blocks). + +**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. ```kotlin class Service { @@ -87,50 +108,59 @@ class Service { } ``` -**Rule 3 (type parameters)**: nested type aliases may _not_ refer to the type parameters of the enclosing classifier. Inner type aliases may do. In both cases type parameters may be included as part of the type alias declaration itself. +**Rule 4 (non-capturing)**: nested type aliases may _not_ capture type parameters of the enclosing classifier. -```kotlin -class Example { - // this alias is not allowed to capture `T` - typealias Foo = List +> [!TIP] +> 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. - // these aliases capture the type parameter `T` - inner typealias Bar = List - inner typealias Qux = Map +We formally define the set of captured type parameters of a type `T` with enclosing parameters `P`, `captured(T, P)`, as follows. - // inner type aliases may, but need not capture - inner typealias Moo = Int -} -``` +- If `T` is a type parameter, `capture(T, P) = { T }`; +- If `T` is a nested type access `A.B`, `capture(T, P) = capture(B, capture(A, P))`; +- If `T` is an inner type `I`, `capture(T, P) = capture(A, P) + ... + capture(Z, P) + P`; +- If `T` is of the form `C`, with `C` not inner, or `(A, ..., Y) -> Z`, `capture(T, P) = capture(A, P) + ... + capture(Z, P)`; +- If `T` is a nullable type `R?`, `capture(T, P) = capture(R, P)`; +- If `T` is `*`, then `capture(*, P) = { }`; +- 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. -We refer to nested and inner typealiases in the same way that we do with other nested and inner classifiers. +For a generic nested type alias declaration, ```kotlin -fun example1( - foo: Example.Foo, - bar: Example.Bar, - qux: Example.Qux, - moo: Example.Moo, -) +class Outer { + typealias Alias = Rhs +} ``` -**Rule 4 (inner classes)**: type aliases which refer to inner classes within the same classifier and do not prefix it with a path must be marked as `inner`. The reasoning behind it is that inner classes implicitly capture the type parameters of the outer class. - -- Note that it is possible to create a nested (not inner!) type alias when a full type is given. +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 }`. ```kotlin class Example { - inner class Inner + // capture(List, { T }) = { } ⊆ { } => OK + typealias Foo = List - inner typealias One = Inner - typealias Two = Example.Inner -} + // capture(List, { T }) = { T } ⊈ { } => not allowed + typealias Bar = List + + // capture(List, { T }) = { A } ⊆ { A } => OK + typealias Baz = List + + // capture(Map, { T }) = { T, A } ⊈ { A } => not allowed + typealias Qux = Map + + + inner class Inner { } + + // capture(Inner, { T }) + // = capture(Int) + { T } + // = { T } ⊈ { T } => not allowed + typealias Moo = Inner -fun example2( - inner: Example.Inner, - one: Example.One, - two: Example.Two, // expands to Example.Inner -) + // capture(Example.Inner, { T }) + // = capture(Inner, capture(Example, { T })) + // = capture(Inner, { S }) + // = capture(Int) + { S } = { S } ⊆ { S } => OK + typelias Boo = Example.Inner +} ``` ### Reflection @@ -139,8 +169,43 @@ The main reflection capabilities in [`kotlin.reflect`](https://kotlinlang.org/ap 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. -## Design questions +### Multiplatform + +Kotlin supports [`expect` and `actual` declarations](https://kotlinlang.org/docs/multiplatform-expect-actual.html) for Multiplatform development. + +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`. + +We propose to completely forbid nested type aliases to take part on the actualization process. That means that: + +- The prohibition about `expect typealias` also covers nested type aliases. +- It is not possible to actualize an `expect` nested class with an `actual` nested type alias. + +Note that this restriction needs to be checked whenever a top-level `expect` class is actualized by a type alias. -**Expect / actual**: should we allow nested `expect` (and correspondingly, `actual`) type aliases? +```kotlin +// expect.kt +expect class E { + expect class I +} + +// actual1.kt +actual class E { + actual typealias I = Int // 'actual typealias' not allowed +} + +// actual2.kt +class A { + typealias I = Int +} + +actual typealias E = A // actualizing nested 'expect class' with typealias not allowed + +// actual3.kt +class B { + class I +} + +actual typealias E = B // ok +``` -- This is very close to abstracting over types, which is a non-goal of this KEEP. \ No newline at end of file +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`. From b8226c472fc48aeb5114b98d78b6680f8d20ce07 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 19 Dec 2024 12:04:49 +0100 Subject: [PATCH 10/17] Information about inner classes --- proposals/nested-typealias.md | 50 ++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index cfe6ccc2c..9bfe92230 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -127,12 +127,14 @@ For a generic nested type alias declaration, ```kotlin class Outer { - typealias Alias = Rhs + typealias Alias = Rhs } ``` 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 }`. +The following nested type aliases exemplify this calculation. + ```kotlin class Example { // capture(List, { T }) = { } ⊆ { } => OK @@ -163,6 +165,39 @@ class Example { } ``` +**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. + +```kotlin +// declaration.kt +class A { + inner class B { } + + typealias I = B + // generates the following "type alias constructor" + // here "static" is pseudo-syntax only + static fun A.I() = A.B() +} + +class C { + typealias D = A.B + // generates the following "type alias constructor" + // here "static" is pseudo-syntax only + static fun A.D() = A.B() +} + +// incorrectUsage.kt +val i = A().I() // ⚠️ `I` lives in the static scope of `A` +val d = A().C.D() // ⚠️ cannot use `C.D()` to refer to a function + +// correctUsage.kt +import A.* // imports `I` +import C.* // imports `D` + +val d = A().D() +``` + +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()`. + ### Reflection 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. @@ -178,29 +213,24 @@ For top-level declarations, it is forbidden to create a `expect typealias`, but We propose to completely forbid nested type aliases to take part on the actualization process. That means that: - The prohibition about `expect typealias` also covers nested type aliases. -- It is not possible to actualize an `expect` nested class with an `actual` nested type alias. +- It is not possible to actualize a nested class with a nested type alias. Note that this restriction needs to be checked whenever a top-level `expect` class is actualized by a type alias. ```kotlin // expect.kt expect class E { - expect class I -} - -// actual1.kt -actual class E { - actual typealias I = Int // 'actual typealias' not allowed + class I } -// actual2.kt +// actualIncorrect.kt class A { typealias I = Int } actual typealias E = A // actualizing nested 'expect class' with typealias not allowed -// actual3.kt +// actualCorrect.kt class B { class I } From 70dc8f8cf52a38e68ee04c6133dc2716392c0e59 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 20 Dec 2024 18:29:48 +0100 Subject: [PATCH 11/17] Add link to discussion issue --- proposals/nested-typealias.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index 9bfe92230..249407273 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -3,8 +3,8 @@ * **Type**: Design proposal * **Author**: Alejandro Serrano * **Contributors**: Ivan Kochurkin -* **Discussion**: -* **Status**: +* **Discussion**: [KEEP-406](https://github.com/Kotlin/KEEP/issues/406) +* **Status**: In discussion * **Related YouTrack issue**: [KT-45285](https://youtrack.jetbrains.com/issue/KT-45285/Support-nested-and-local-type-aliases) ## Abstract From 18f93361ed9077fbc3609ffe52d3e942d088c835 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 23 Dec 2024 09:36:35 +0100 Subject: [PATCH 12/17] Update proposals/nested-typealias.md Co-authored-by: Luca Kellermann --- proposals/nested-typealias.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index 249407273..e8996b6bd 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -34,7 +34,7 @@ Currently, type aliases may only be declared at the top level. This hinders thei ```kotlin class Dijkstra { - typelias VisitedNodes = Set + typealias VisitedNodes = Set private fun step(visited: VisitedNodes, ...) = ... } From 8a41671b888d8041a38539d8427b7247d1999f32 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 23 Dec 2024 09:36:43 +0100 Subject: [PATCH 13/17] Update proposals/nested-typealias.md Co-authored-by: Luca Kellermann --- proposals/nested-typealias.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index e8996b6bd..57095b753 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -161,7 +161,7 @@ class Example { // = capture(Inner, capture(Example, { T })) // = capture(Inner, { S }) // = capture(Int) + { S } = { S } ⊆ { S } => OK - typelias Boo = Example.Inner + typealias Boo = Example.Inner } ``` From d3785d654d16e0ec99c649213631a12690c8db7b Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 23 Dec 2024 09:36:54 +0100 Subject: [PATCH 14/17] Update proposals/nested-typealias.md Co-authored-by: Luca Kellermann --- proposals/nested-typealias.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index 57095b753..6215e2154 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -153,8 +153,8 @@ class Example { inner class Inner { } // capture(Inner, { T }) - // = capture(Int) + { T } - // = { T } ⊈ { T } => not allowed + // = capture(Int, { T }) + { T } + // = { T } ⊈ { } => not allowed typealias Moo = Inner // capture(Example.Inner, { T }) From cccc6b63b534b62fc094e22da0202a06249d0613 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 23 Dec 2024 09:37:03 +0100 Subject: [PATCH 15/17] Update proposals/nested-typealias.md Co-authored-by: Luca Kellermann --- proposals/nested-typealias.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index 6215e2154..a62f63805 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -113,7 +113,7 @@ class Service { > [!TIP] > 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. -We formally define the set of captured type parameters of a type `T` with enclosing parameters `P`, `captured(T, P)`, as follows. +We formally define the set of captured type parameters of a type `T` with enclosing parameters `P`, `capture(T, P)`, as follows. - If `T` is a type parameter, `capture(T, P) = { T }`; - If `T` is a nested type access `A.B`, `capture(T, P) = capture(B, capture(A, P))`; From 8152bc8c541dceabaf14a941162e7a9a25193a51 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 23 Dec 2024 17:15:08 +0100 Subject: [PATCH 16/17] Fix definition for `capture` --- proposals/nested-typealias.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index a62f63805..3d9ce1eb7 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -116,7 +116,7 @@ class Service { We formally define the set of captured type parameters of a type `T` with enclosing parameters `P`, `capture(T, P)`, as follows. - If `T` is a type parameter, `capture(T, P) = { T }`; -- If `T` is a nested type access `A.B`, `capture(T, P) = capture(B, capture(A, P))`; +- If `T` is a nested type access `A.B`, `capture(T, P) = C + capture(B, C)` where `C = capture(A, P))`; - If `T` is an inner type `I`, `capture(T, P) = capture(A, P) + ... + capture(Z, P) + P`; - If `T` is of the form `C`, with `C` not inner, or `(A, ..., Y) -> Z`, `capture(T, P) = capture(A, P) + ... + capture(Z, P)`; - If `T` is a nullable type `R?`, `capture(T, P) = capture(R, P)`; @@ -158,9 +158,9 @@ class Example { typealias Moo = Inner // capture(Example.Inner, { T }) - // = capture(Inner, capture(Example, { T })) - // = capture(Inner, { S }) - // = capture(Int) + { S } = { S } ⊆ { S } => OK + // = capture(Example, { T }) + capture(Inner, capture(Example, { T })) + // = { S } + capture(Inner, { S }) + // = { S } + capture(Int, { S }) + { S } = { S } ⊆ { S } => OK typealias Boo = Example.Inner } ``` @@ -193,6 +193,7 @@ val d = A().C.D() // ⚠️ cannot use `C.D()` to refer to a function import A.* // imports `I` import C.* // imports `D` +val i = A().I() val d = A().D() ``` From 3d770f798964156d592720599fe81e155fe9be2c Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 10 Jan 2025 11:08:48 +0100 Subject: [PATCH 17/17] Add more explanation for `capture` --- proposals/nested-typealias.md | 41 ++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/proposals/nested-typealias.md b/proposals/nested-typealias.md index 3d9ce1eb7..89a9b2fa3 100644 --- a/proposals/nested-typealias.md +++ b/proposals/nested-typealias.md @@ -113,14 +113,14 @@ class Service { > [!TIP] > 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. -We formally define the set of captured type parameters of a type `T` with enclosing parameters `P`, `capture(T, P)`, as follows. - -- If `T` is a type parameter, `capture(T, P) = { T }`; -- If `T` is a nested type access `A.B`, `capture(T, P) = C + capture(B, C)` where `C = capture(A, P))`; -- If `T` is an inner type `I`, `capture(T, P) = capture(A, P) + ... + capture(Z, P) + P`; -- If `T` is of the form `C`, with `C` not inner, or `(A, ..., Y) -> Z`, `capture(T, P) = capture(A, P) + ... + capture(Z, P)`; -- If `T` is a nullable type `R?`, `capture(T, P) = capture(R, P)`; -- If `T` is `*`, then `capture(*, P) = { }`; +We formally define the set of captured type parameters of a type `A` with enclosing parameters `P`, `capture(A, P)`, as follows. + +- If `A` is a type parameter `T`, `capture(T, P) = { T }`; +- If `A` is a nested type access `Outer.Inner`, `capture(Outer.Inner, P) = FromOuter + capture(Inner, FromOuter)` where `FromOuter = capture(Outer, P))`; +- If `A` is an inner type with type arguments `Inner`, `capture(Inner, P) = capture(B, P) + ... + capture(Z, P) + P`; +- If `A` is a non-inner type with type arguments `Class` or a function type `(B, ..., Y) -> Z`, `capture(A, P) = capture(B, P) + ... + capture(Z, P)`; +- If `A` is a nullable type `B?`, `capture(B?, P) = capture(B, P)`; +- If `A` is `*`, then `capture(*, P) = { }`; - 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. For a generic nested type alias declaration, @@ -133,35 +133,46 @@ class Outer { 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 }`. -The following nested type aliases exemplify this calculation. +The following nested type aliases exemplify this calculation, and describe the intuition behind those results. ```kotlin class Example { - // capture(List, { T }) = { } ⊆ { } => OK + // should be allowed, no type is captured here typealias Foo = List + // capture(List, { T }) = { } ⊆ { } => OK - // capture(List, { T }) = { T } ⊈ { } => not allowed + // should be rejected, since `T` (an argument to the outer `Example`) + // is explicitly mentioned typealias Bar = List + // capture(List, { T }) = { T } ⊈ { } => not allowed - // capture(List, { T }) = { A } ⊆ { A } => OK + // should be allowed, since every type parameter (`A`) + // comes from the type alias itself typealias Baz = List + // capture(List, { T }) = { A } ⊆ { A } => OK - // capture(Map, { T }) = { T, A } ⊈ { A } => not allowed + // should be rejected, since `T` is explicitly mentioned typealias Qux = Map + // capture(Map, { T }) = { T, A } ⊈ { A } => not allowed inner class Inner { } + // should be rejected, since we mention `Inner` + // which has an outer `Example` with `T` as type parameter + typealias Moo = Inner // capture(Inner, { T }) // = capture(Int, { T }) + { T } // = { T } ⊈ { } => not allowed - typealias Moo = Inner + // should be allowed, since we access `Inner` through + // an explicit `Example` which does not capture `T` + typealias Boo = Example.Inner // capture(Example.Inner, { T }) // = capture(Example, { T }) + capture(Inner, capture(Example, { T })) // = { S } + capture(Inner, { S }) // = { S } + capture(Int, { S }) + { S } = { S } ⊆ { S } => OK - typealias Boo = Example.Inner + } ```