From aadaedd0390c5fa9fca774d2c51200a0d93725b8 Mon Sep 17 00:00:00 2001 From: Pavel Mikhailovskii Date: Thu, 4 Aug 2022 19:08:50 +0200 Subject: [PATCH 1/9] Add a proposal for "data objects" --- proposals/data-objects.md | 76 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 proposals/data-objects.md diff --git a/proposals/data-objects.md b/proposals/data-objects.md new file mode 100644 index 000000000..d92a3caa6 --- /dev/null +++ b/proposals/data-objects.md @@ -0,0 +1,76 @@ +# Data objects + +* **Type**: Design proposal +* **Authors**: Alexander Udalov, Roman Elizarov, Pavel Mikhailovskii +* **Status**: Stable since Kotlin 1.3 (Revision 3.3), experimental in Kotlin 1.1-1.2 + + +## Introduction + +When using algebraic data types, it’s annoying to declare singleton values because there’s no way to get automatic toString similar to data classes. E.g. on JVM: +```kotlin +sealed class List { + data class Cons(val value: T) : List() + object Nil : List() +} + +fun main() { +println(List.Cons(42)) // Cons(value=42) +println(List.Nil) // test.List$Nil@1d251891 +} +``` + +A workaround for the user is either to declare `toString `manually, which leads to boilerplate, or use data class with a placeholder default argument, which seems bizarre. + +## Other aspects + +* Default toString implementation differs on JVM, JS, Native + * In JS, toString returns [object Object] + * Native behaves similarly to JVM, but inner classes are delimited with ., i.e. test.List*.*Nil@1d251891 in the example above +* The @hash part is meaningless even for non-ADT objects, because object is a singleton +* Can’t declare an empty @JvmRecord class in Kotlin (KT-48155 (https://youtrack.jetbrains.com/issue/KT-48155/JvmRecord-does-not-allow-one-to-define-no-parameters)) + * Probably not a problem + +### toString for all objects + +One possible solution is to generate toString for all objects unconditionally, which returns the simple (unqualified) name of the object. + +The advantage is that it’s more uniform, and the least surprising for the users. + +There are some issues though: + +* It’s likely a major breaking change +* It leads to extra bytecode which is often unnecessary +* Simple name instead of FQ name might be confusing, especially for cases like companion object + +### Data object + +Another possible solution is to allow the data modifier on objects, which will lead to toString being generated as for data classes. + +Other data class methods should not be generated: + +* equals and hashCode work fine already since it’s a singleton +* componentN makes no sense because it doesn’t have a public constructor +* copy makes no sense because it’s a singleton + * Or should it be generated and always return this? + +Question: data companion object + +Question: could Unit be redefined as data object? + +## The Proposed Solution + +* Support `data object` syntax in all frontends & backends +* Generate equals, hashCode, toString for data objects + * `equals` should only check whether `this` and the other object are of the same type + * `hashCode` returns the value of the hash code of the qualified class name + * `toString` returns the simple name of the object +* On JVM: if data object is serializable, generate `readResolve` which returns the value of the INSTANCE field +* Prohibit to declare or inherit custom `equals`/`hashCode` (but not `toString`!) in data objects + * It’s OK if there’s a non-final implementation of `equals`/`hashCode` in a superclass of a data object, since it’s overridden anyway +* Prohibit `data companion object` syntax +* After kotlin-stdlib is compiled with language version 1.8, make `Unit` and `EmptyCoroutineContext` data objects +* IDE (KTIJ-22087 (https://youtrack.jetbrains.com/issue/KTIJ-22087/Support-IDE-inspections-for-upcoming-data-objects)) + * Existing inspection that suggests to add data to a sealed subclass should also do it for objects now + * Add an inspection to recommend to add data to a serializable object + From 4e0c7afd17b382538181f76b9301c8fed74315ca Mon Sep 17 00:00:00 2001 From: Pavel Mikhailovskii Date: Thu, 4 Aug 2022 19:48:26 +0200 Subject: [PATCH 2/9] Correct "status" field of the data objects proposal --- proposals/data-objects.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/data-objects.md b/proposals/data-objects.md index d92a3caa6..8abba04f9 100644 --- a/proposals/data-objects.md +++ b/proposals/data-objects.md @@ -2,7 +2,7 @@ * **Type**: Design proposal * **Authors**: Alexander Udalov, Roman Elizarov, Pavel Mikhailovskii -* **Status**: Stable since Kotlin 1.3 (Revision 3.3), experimental in Kotlin 1.1-1.2 +* **Status**: Experimental since 1.7.20, stable since 1.8.0 ## Introduction From e923c0786426d05e69109581a8676a2f0ea851b7 Mon Sep 17 00:00:00 2001 From: Pavel Mikhailovskii Date: Mon, 15 Aug 2022 13:34:21 +0200 Subject: [PATCH 3/9] Fix formatting and naming in the example code --- proposals/data-objects.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/proposals/data-objects.md b/proposals/data-objects.md index 8abba04f9..7a041ff23 100644 --- a/proposals/data-objects.md +++ b/proposals/data-objects.md @@ -9,14 +9,14 @@ When using algebraic data types, it’s annoying to declare singleton values because there’s no way to get automatic toString similar to data classes. E.g. on JVM: ```kotlin -sealed class List { - data class Cons(val value: T) : List() - object Nil : List() +sealed class MyList { + data class Cons(val value: T) : MyList() + object Nil : MyList() } fun main() { -println(List.Cons(42)) // Cons(value=42) -println(List.Nil) // test.List$Nil@1d251891 + println(MyList.Cons(42)) // Cons(value=42) + println(MyList.Nil) // test.MyList$Nil@1d251891 } ``` From 97eada1c621fd3c5ccada8f093092f361e06f1f3 Mon Sep 17 00:00:00 2001 From: Pavel Mikhailovskii Date: Mon, 15 Aug 2022 13:40:08 +0200 Subject: [PATCH 4/9] Add a link to the issue --- proposals/data-objects.md | 1 + 1 file changed, 1 insertion(+) diff --git a/proposals/data-objects.md b/proposals/data-objects.md index 7a041ff23..b5d663d1b 100644 --- a/proposals/data-objects.md +++ b/proposals/data-objects.md @@ -3,6 +3,7 @@ * **Type**: Design proposal * **Authors**: Alexander Udalov, Roman Elizarov, Pavel Mikhailovskii * **Status**: Experimental since 1.7.20, stable since 1.8.0 +* **Discussion and feedback**: [#317](https://github.com/Kotlin/KEEP/issues/317) ## Introduction From cb126696ada133627d82af0d81868e236ac92146 Mon Sep 17 00:00:00 2001 From: Pavel Mikhailovskii Date: Mon, 15 Aug 2022 13:52:07 +0200 Subject: [PATCH 5/9] Explain the need for `equals` and `hashCode` for data objects --- proposals/data-objects.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/proposals/data-objects.md b/proposals/data-objects.md index b5d663d1b..a533b9727 100644 --- a/proposals/data-objects.md +++ b/proposals/data-objects.md @@ -46,15 +46,22 @@ There are some issues though: ### Data object -Another possible solution is to allow the data modifier on objects, which will lead to toString being generated as for data classes. +Another possible solution is to allow the data modifier on objects, which will lead to `toString` being generated as for data classes. + +Even though data objects are singletons, `equals` and `hashCode` probably should also be generated: +* it may be useful to have a stable `hashCode` +* as it's in principle possible to create multiple instances of a data objec` class via JVM reflection, and some third-party frameworks may do that, +it would make sense to have `equals` implementation that would treat all instances of the same data class as equal. Other data class methods should not be generated: -* equals and hashCode work fine already since it’s a singleton -* componentN makes no sense because it doesn’t have a public constructor -* copy makes no sense because it’s a singleton +* `componentN` makes no sense because it doesn’t have a public constructor +* `copy` makes no sense because it’s a singleton * Or should it be generated and always return this? + + + Question: data companion object Question: could Unit be redefined as data object? From 4ebf43b8e86998e6b89464af389960d98f212c36 Mon Sep 17 00:00:00 2001 From: Pavel Mikhailovskii Date: Wed, 17 Aug 2022 14:58:29 +0200 Subject: [PATCH 6/9] Data objects: Cover object expressions --- proposals/data-objects.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/data-objects.md b/proposals/data-objects.md index a533b9727..380d020d9 100644 --- a/proposals/data-objects.md +++ b/proposals/data-objects.md @@ -76,7 +76,7 @@ Question: could Unit be redefined as data object? * On JVM: if data object is serializable, generate `readResolve` which returns the value of the INSTANCE field * Prohibit to declare or inherit custom `equals`/`hashCode` (but not `toString`!) in data objects * It’s OK if there’s a non-final implementation of `equals`/`hashCode` in a superclass of a data object, since it’s overridden anyway -* Prohibit `data companion object` syntax +* Prohibit `data companion object` syntax and use in anonymous object expressions, e.g. `val o = data object {}` * After kotlin-stdlib is compiled with language version 1.8, make `Unit` and `EmptyCoroutineContext` data objects * IDE (KTIJ-22087 (https://youtrack.jetbrains.com/issue/KTIJ-22087/Support-IDE-inspections-for-upcoming-data-objects)) * Existing inspection that suggests to add data to a sealed subclass should also do it for objects now From 8042f0a8a170bb8fbcc28adac31deac042478df5 Mon Sep 17 00:00:00 2001 From: Pavel Mikhailovskii Date: Thu, 18 Aug 2022 20:32:59 +0200 Subject: [PATCH 7/9] Fix a typo Co-authored-by: Niklas Baudy --- proposals/data-objects.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/data-objects.md b/proposals/data-objects.md index 380d020d9..3ec310ebc 100644 --- a/proposals/data-objects.md +++ b/proposals/data-objects.md @@ -50,7 +50,7 @@ Another possible solution is to allow the data modifier on objects, which will l Even though data objects are singletons, `equals` and `hashCode` probably should also be generated: * it may be useful to have a stable `hashCode` -* as it's in principle possible to create multiple instances of a data objec` class via JVM reflection, and some third-party frameworks may do that, +* as it's in principle possible to create multiple instances of a `data object` class via JVM reflection, and some third-party frameworks may do that, it would make sense to have `equals` implementation that would treat all instances of the same data class as equal. Other data class methods should not be generated: From a955215eb329af6307e2a03ad6aef595a291347d Mon Sep 17 00:00:00 2001 From: Pavel Mikhailovskii Date: Tue, 30 Aug 2022 13:56:06 +0200 Subject: [PATCH 8/9] Remove blank lines --- proposals/data-objects.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/proposals/data-objects.md b/proposals/data-objects.md index 3ec310ebc..681b4e606 100644 --- a/proposals/data-objects.md +++ b/proposals/data-objects.md @@ -59,9 +59,6 @@ Other data class methods should not be generated: * `copy` makes no sense because it’s a singleton * Or should it be generated and always return this? - - - Question: data companion object Question: could Unit be redefined as data object? From 2cbbe7c2e358afbc9420ffad946ada1b94358996 Mon Sep 17 00:00:00 2001 From: Pavel Mikhailovskii Date: Tue, 30 Aug 2022 14:06:09 +0200 Subject: [PATCH 9/9] Specify the behaviour of KClass.isData for data objects --- proposals/data-objects.md | 1 + 1 file changed, 1 insertion(+) diff --git a/proposals/data-objects.md b/proposals/data-objects.md index 681b4e606..c6dd767ae 100644 --- a/proposals/data-objects.md +++ b/proposals/data-objects.md @@ -73,6 +73,7 @@ Question: could Unit be redefined as data object? * On JVM: if data object is serializable, generate `readResolve` which returns the value of the INSTANCE field * Prohibit to declare or inherit custom `equals`/`hashCode` (but not `toString`!) in data objects * It’s OK if there’s a non-final implementation of `equals`/`hashCode` in a superclass of a data object, since it’s overridden anyway +* `DataObject::class.isData` should return `true` * Prohibit `data companion object` syntax and use in anonymous object expressions, e.g. `val o = data object {}` * After kotlin-stdlib is compiled with language version 1.8, make `Unit` and `EmptyCoroutineContext` data objects * IDE (KTIJ-22087 (https://youtrack.jetbrains.com/issue/KTIJ-22087/Support-IDE-inspections-for-upcoming-data-objects))