Data classes should be able to inherit from each other

I think you would also get those things if only the children are data classes.

I know there is a lot written and said about mixed type equality. But the simplest (and to me most logical) solution would just be to only match equal if they are the same class type AND have logically equal properties.

So B inherits A, an instance of B can never equal an instance of A no matter what their common properties are.

That seems like a good default. And if people do need mixed type equality, they can implement it in a way that makes sense for their use case.

2 Likes

The abstract class with data class strategy I described does work, however, it has another draw back I’m discovering.

For easy migration, I attempted to use the @JvmField annotation, however because the properties are declared overriden, it can not be used. So more and more this strategy seems quite bad.

If we did get data class inheritance, we could use normal non-abstract or overriden properties, and thus these annotations would work.

@Wavesonics I agree with you. This behavior makes sense to me, instances from different classes shouldn’t be equal at all.

Man I just keep running into this problem. I actually wish data classes could ONLY inherit from other data classes.

Here is a concrete case I just encountered:

I have message classes passed between client and server (shared code base, so awesome job there Kotlin!)

This is what I want:

open data class NetworkResponse(val responseCode: Int)
open data class GameUpdate(val gameState: GameState, responseCode: Int) : NetworkResponse(responseCode)
data class PlayerJoinUpdate(val newPlayerId: Int, gameState: GameState, responseCode: Int) : GameUpdate(gameState, responseCode)

However, since data classes currently can’t inherit from each other, I have a nasty problem. Only the Leaf nodes in my inheritance tree can be data classes. Which almost makes sense, except in my scheme here, non-leaf nodes such as GameUpdate are still totally valid messages to be sent on their own.

What I have had to do is something more like this:

abstract class NetworkResponse(open val responseCode: Int)
open class GameUpdate(open val gameState: GameState, override val responseCode: Int) : NetworkResponse(responseCode)
data class PlayerJoinUpdate(val newPlayerId, override val gameState: GameState, override val responseCode: Int) : GameUpdate(gameState, responseCode)

Now I have to manually add in all of the data class provided methods into the GameUpdate class which is not fun. And of course multiply this out across the entire protocol, there are a good number of classes I need to do this for, and we’re back closer to Java land, where I get lazy and don’t do it.

Also notice that in this scheme I have to make the properties open, and override them in child classes. I’d much rather just be able to take them in as arguments and pass them to the parent ctor as in my first example.

I think there are some really good use cases for data class inheritance.

I know ilya.gorbunov was concerned about what equality would mean in a world that allowed inheritance, and I think it would be simplest and most useful if it was the strict equality like I mentioned earlier: Only classes of the exact same type, with logically equivalent properties, would be equal to each other. Anything more complex than that, dealing with child classes or what ever, can just be left to the user to implement on their own.

Exmaple:

override fun equals(other: Any?): Boolean
{
    if (this === other) return true
    if (this.javaClass != other?.javaClass) return false
    other as GameUpdate
    if (this.responseCode != other.responseCode) return false
    if (this.gameState != other.gameState) return false
    return true
}
7 Likes

Thanks, this example is helpful.

What methods of a data class do you miss most in these open classes? Is it componentN for destructuring? The copy function? Or toString()?

These are the most important to me:

  • equals important for some of my client side logic, dedupping
  • toString for debugging is very nice

Less so, but would be nice:

  • copy I don’t implement this currently, but if I had it for free I’d probably use it for mutating my immutable classes.
  • hash less so for my particular use case, but probably important if I ended up using them as a key in a hashmap or something.

I haven’t used the destructing stuff for my data classes yet.

1 Like

are there any plans to reconsider inheritance for data classes?
I’m developing logic solvers and need to model a hierarchy of propositions, conjunctions, disjunctions, implications etc. I had to revert from data classes back to ordinary ones and implement all the tedious equals() etc boilerplate just because at some point I really needed inheritance.

1 Like

Is there any reason that, if you are inheriting from another data class, the compiler couldn’t just force you to implement equals in the subclass?

1 Like

I need this too. I am copying and pasting fields and generating technical debt.

.equals() should be defined as: thisClass.equals(otherClass) + current .equals() logic. That would solve the .equals() problem.

There are several valid scenarios to extend data classes. Composition is not always an option. For example, it is not an option in a web server serving JSON. Why should a API client deal with properties inside objects?

Example: User, Player(User): both classes share name, username, email etc, why should an API class need to access to something like p1.user.name, p1.score? It is a valid user scenario of an API user consuming an object using properties like to p1.name, p1.score, etc. aka using a plain object without sub-objects.

User, Player(User): both classes share name, username, email etc

Possible it is better to extract repeating fields to UserDetails?

For example:

data class UserDetails(val name: String, val phone: Long)

sealed class UserAuthResponse {
     data class Moderator(val isAdmin: Boolean, val data: UserDetails): UserAuthResponse()

     data class Player(val isAdmin: Boolean, val data: UserDetails): UserAuthResponse()

     data class Error(val userMessage: String, val code: Int): UserAuthResponse()
}

As you can see above, you don’t need any inheritance here.

Also you can extract interface like interface WithUserDetails { val data: UserDetails } to cover all items with this field.

A bit late to the party, but here are my 2 cents (I haven’t found a similar proposal here yet): I think a good use case for this would be entity classes. You could have a base entity class with an id, which is extended by some auditing class etc.

Now, this does not need to be inheritance. But the current recommended way of using composition is not nice in terms of field access, and nested classes also don’t go very well with entities anyway.

A language which has a better solution to this imo is Go. I know they are two completely different languages, but I feel like the data classes are trying to be more like structs. In Go you can do the following using type embedding:

type BaseEntity struct {
	id int64
}

type AuditEntity struct {
	BaseEntity // notice that there's only a type here

	createdDate time.Time
	createdBy user.User
	// etc
}

type MyEntity struct {
	AuditEntity // again, only a type

	someField int64
	// etc
}

func main() {
	// id, createdDate, createdBy, someField
	x := MyEntity{AuditEntity{BaseEntity{someId}, someDate, someUser}, someField}
	log.Println(x.id) // someId
	
	y := MyEntity{}
	log.Println(y.id) // 0
}

You can then access these fields directly on the struct. I think such a pattern could also work for data classes. I don’t think you’d have the equals problem, as this embedding is just an implicit composition as is recommended for Kotlin now, with the type name as field name.

An improvement over even Go could be improving the constructor so that you don’t have to create the nested structs explicitly.

Interesting idea. It also wouldn’t be to hard to build a prototype of this using kapt. At the end it could look something like this

data class BaseEntity(val id: Int) 
data class MyEntity(@Embded val base: BaseEntity, val someOtherField: Int)

Kapt could then be used to create the getters and setters and even some “fake” constructor.

val MyEntity.id get() = base.id
fun MyEntity(id: Int, someOtherField: Int): MyEntity(BaseEntity(id), someOtherField)

Yep, I might actually try that soon (repo: https://github.com/RobbinBaauw/DataClassEmbeddings)

I would like to note that besides the data, it also goes for functions defined for this struct. Instead of inheritance, there is another way to resolving name conflicts:

Embedding types introduces the problem of name conflicts but the rules to resolve them are simple. First, a field or method X hides any other item X in a more deeply nested part of the type. If log.Logger contained a field or method called Command, the Command field of Job would dominate it.

Docs

Meaning: the top level is more important. I think something like this also makes sense for data classes, as you only generate this proxy (for getters/setters/functions) if there are no duplicates.

Edit: this may work, though things like Hibernate will probably not like this since annotations on these fields aren’t copied if we use getters/setters.

In Java, lombok project has @Data annotations, that generate equals method with “canEquals” system and there’s no problem with inheritance.
So, i can’t see why there’s a problem with that for kotlin data class ?