30

The following does not work, but hopefully helps you understand what I mean:

class Example<T : DataClass>

In case you would like to know what I am trying to accomplish, this is an example of what I had in mind:

class Repository<T> where T : Entity, // Entity defines mutable property 'id'
                          T : DataClass {

  // assume there is a map here

  fun add(obj: T) {
    val copy = obj.copy(id = generateID())
    map.put(copy.id, copy)
  }

}

Or is there a better way to accomplish what I am trying to do?

4 Answers4

17

I have the feeling that what you actually want is that T should be able to copy itself with a new ID, and have an ID. Not necessarily that it is a data class. So you could just use an interface to define that.

For example:

interface CopyableWithId<out T> where T: CopyableWithId<T> {
    fun copy(newId: Long): T
    val id: Long
}

data class BarBaz(override var id: Long, var name: String): CopyableWithId<BarBaz> {
    override fun copy(newId: Long): BarBaz = copy(id = newId)
}

class Repository<T> where T : CopyableWithId<T>{

    val map: MutableMap<Long, CopyableWithId<T>> = HashMap()

    fun add(obj: T) {
        val copy = obj.copy(generateID())
        map.put(copy.id, copy)
    }

    private fun generateID(): Long {
        return 1L
    }
}
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
5

No, data classes don't have any specific representation in type system and cannot be distinguished from regular classes (similar question).

You can, however, require the methods a data class with certain number of components has using an interface (actually it will be a marker interface on data classes).

Here's an example for data classes with two components:

interface Data2<T1, T2> {
    operator fun component1(): T1
    operator fun component2(): T2
    fun copy(t1: T1, t2: T2): Data2<T1, T2>
}

toString, hashCode and equals can be called on any type anyway.

Then just mark your data class with the interface:

data class Impl(val i: Int, val s: String): Data2<Int, String>

val d: Data2<Int, String> = Impl(1, "2")
val (c1, c2) = d
val copy = d.copy(-1, d.component2())

copy function is not completely type-safe because Kotlin doesn't have self type (and no way to require interface implementations to be subtype of a specific type), but if you only mark your data classes with it, it should work (see another option below).

Another drawback is that you lose default parameters of copy method and have to call it with all the parameters specified:

val d = myD2.copy(newValue, myD2.component2())

Another option is to define these interfaces as Data2<T1, T2, out Self>, class Impl(...): Data2<..., Impl>, and make copy return Self, but it won't make it any better if you use the interface as Data2<SomeType, SomeType, *>.

Community
  • 1
  • 1
hotkey
  • 140,743
  • 39
  • 371
  • 326
4

You also can implement copy or component1, component2 more generalised way.

For example:

    interface Copyable <T> {
        fun copy(fields: T.() -> T): T
    }

    data class BarBaz(var id: Long, var name: String): Copyable<BarBaz> {
        override fun copy(fields: BarBaz.() -> BarBaz): BarBaz {
           val instance = fields(this)
           return copy(id = instance.id, name = instance.name)
        }
    }

class Repository<T> where T : Copyable<T>{

    val map: MutableMap<Long, Copyable<T>> = HashMap()

    fun add(obj: T) {
        val copy = obj.copy{id = generateID()}
        map.put(copy.id, copy)
    }

    private fun generateID(): Long {
        return 1L
    }
}
Arsenius
  • 4,972
  • 4
  • 26
  • 39
  • Looks great but doesn't work. Inside method `add` you don't have knowledge about `BarBaz` so you don't have knowledge of field `id`. You can add that knowledge to `Copyable` but it won't change much, you still won't be able to construct copy inside `copy` lambda. – Jacek Pietras Mar 25 '23 at 13:23
0

Could be unrelated because I had similar but slightly different problem.

I need to have moved shared logic to a super class and the issue was that I can't use copy method of generic T. I found this workaround:

Entity:

data class MyEntity(
    val id: String,
    val createdAt: Instant,
    val updatedAt: Instant
)

Abstract Generic Repository:

abstract class GenericRepository<T> {

    abstract val copyFn: KCallable<T>

    fun add(obj: T) {
        val instanceParameter = copyFn.instanceParameter!!
        val idParameter = copyFn.findParameterByName("id")!!
        val copy = copyFn.callBy(
            mapOf(
                instanceParameter to obj,
                idParameter to "new id"
            )
        )
        // Do whatever you want with the copy
    }
}

Or cleaner and more generic version of Abstract Generic Repository:

abstract class BetterGenericRepository<T> {

    abstract val copyFn: KCallable<T>

    fun add(obj: T): T {
        val instanceParameter = getInstanceParameter()
        val idParameter = getParameterByName(instanceParameter, "id")
        val updatedAtParameter = getParameterByName(instanceParameter, "updatedAt")
        val copy = copyFn.callBy(
            mapOf(
                instanceParameter to obj,
                idParameter to "new id",
                updatedAtParameter to Instant.now()
            )
        )
        // Do whatever you want with the copy
        return copy
    }

    private fun getInstanceParameter() =
        copyFn.instanceParameter
            ?: throw RuntimeException("${copyFn.returnType} must be Data Class or its method '${copyFn.name}' must have 'instanceParameter' as KParameter")

    private fun getParameterByName(instanceParameter: KParameter, name: String) =
        copyFn.findParameterByName(name)
            ?: throw RuntimeException("${instanceParameter.type} must have '$name' property")
}

Specific implementation of Abstract Repository

class MyRepository: BetterGenericRepository<MyEntity>() {
    override val copyFn = MyEntity::copy
}

And simple check:

fun main() {
    val repository = MyRepository()
    val entity = MyEntity(
        id = "1",
        createdAt = Instant.EPOCH,
        updatedAt = Instant.EPOCH
    )
    println(entity)
    println(repository.add(entity))
}

The result

MyEntity(id=1, createdAt=1970-01-01T00:00:00Z, updatedAt=1970-01-01T00:00:00Z)
MyEntity(id=new id, createdAt=1970-01-01T00:00:00Z, updatedAt=2020-08-26T13:29:42.982Z)
Anton
  • 121
  • 1
  • 3