Data Class Composition without Inheritance

Data class “inheritance” is one of the most sought-after features of Kotlin. Just take a look at the amount of discussion and upvotes across these sites:

I’m aware that traditional inheritance for data class would cause many issues, such as inconsistent componentN functions, ambiguous copy signatures, impossible to define a correct .equals implementation, and just generally breaking the Liskov substitution principle. Nevertheless, developers often want to compose their data models, often to “extend” / augment a data class with just one or two additional fields.

Current work-arounds for a lack of data class extensibility include:

  1. Declaring a base abstract class with shared fields, implementing the base class in two separate data classes.
  2. Declaring a base interface with the shared fields, implementing the interface in two separate data classes.

But these methods involve repeating the full set of fields in the base definition at least three times – once in the base class/interface, and once in both implementations. This is cumbersome and tedious, especially with large data models with dozens of fields, and makes the process to add new fields error-prone.

I would love to see Kotlin have some way to support data class composition without repetition, i.e. define a data class using all of the fields from another data class, plus some additional fields. Without being overly prescriptive, because I am open to any solution, I think one potential way to accomplish this is to define an augments keyword, which embeds the parameters of another class’ constructor into a new class definition:

data class MyModel(
  val prop1: String,
  val prop2: String,
)

data class MyAugmentedModel(
  val prop3: String,
) augments MyModel

val augmented = MyAugmentedModel("prop1", "prop2", "prop3")

Note that there is no inheritance happening here. Any instance of MyAugmentedModel will not also be an instance of MyModel, because they are separate data class and can never be considered equal. It could be helpful to automatically generate extension functions to convert between these two overlapping models, however:

fun MyAugmentedModel.toMyModel() = 
  MyModel(this.prop1, this.prop2)

fun MyModel.toMyAugmentedModel(prop3: String) = 
  MyAugmentedModel(this.prop1, this.prop2, prop3)

I am initially proposing augments MyModel, instead of typical : MyModel(), to avoid ambiguity with the existing syntax which defines inheritance and requires repetition of constructor parameters. extends could cause confusion with Java’s extends, which imlplies inheritance.

The augments syntax as a way to create constructors that have all the fields of another constructor could be useful outside of the contexts of data classes as well, because repetitive constructors is not just a data class problem, but I haven’t thought enough about that yet.

Perhaps take a look at https://youtrack.jetbrains.com/issue/KT-15471/Provide-support-for-dataarg-and-named-spread-convention and https://youtrack.jetbrains.com/issue/KT-8214/Allow-kind-of-vararg-but-for-data-class-parameter which try to solve the same problem with a dataarg parameter modifier that hence will let you do:

data class MyAugmentedModel(
  dataarg val myModel: MyModel,
  val prop3: String,
)
2 Likes

Thanks @kyay10, those are very useful links, and does look like it would solve the problem I described! In probably a better way than the idea I had proposed as well, but no surprise there. :slightly_smiling_face:

Judging by this comment, it seems this feature is prioritized behind the K2 compiler, so I’ll just have to wait. :smiling_face_with_tear:

1 Like