https://kotlinlang.org logo
p

PHondogo

04/27/2021, 9:47 AM
I like how data class auto generate copy function with optional parameters to change any field on copy. It will be very good if such possibility will appear in non data classes, for example annotated with some annotation.
e

elizarov

04/27/2021, 9:48 AM
What's your use-case? Why cannot you just make it
data
?
p

PHondogo

04/27/2021, 9:57 AM
Data class has constraints on inheritence and include autogenerated equals, hash code, toString, that is not needed in some cases. Use case is the event system with event class deep inheritance and needed ability to copy and resend events with new values for some fields.
r

Roukanken

04/27/2021, 9:57 AM
the problem is, such function requires some features from the class... For example you can't make such function (automatically), unless the class stores all parameters from it's primary constructor - which data class is required to do, but non-data one no it also can't be an open class, because then
copy()
would not know which constructor to use - and since it's parameters depend on constructor... Anyways, point is, I don't think there is any data class requirement that you can break, but still be able to generate
copy()
function - if you know of some, share
t

thana

04/27/2021, 10:11 AM
so in the end we would end up with a kotlin version for lombok, right?
r

Roukanken

04/27/2021, 10:21 AM
basically, but ye, other features of data class could be Lombok-able for other classes, if there is big enough demand, tho annotations as features are always a bit dirty but while it's sometimes annoying that data class can't be open, but if it could be; it wouldn't be able to have
copy
which would be imho a bigger loss
p

PHondogo

04/27/2021, 10:22 AM
Copy function in subclasses can autogenerate overrides (new fields just copy as is), and if subclasss also have copy annotation and have additional fields, then generate another copy method including new fields (need to think how to deal with overload resolving).
r

Roukanken

04/27/2021, 10:52 AM
oki, you gave me a bit of work, I had to code it to see the problem Here's the classes with the (supposedly) generated methods:
Copy code
@Target(AnnotationTarget.CLASS)
annotation class Copy

@Copy
open class Parent(
    val a: Int,
) {
    open fun copy(a: Int? = null): Parent {
        return Parent(
            a = a ?: this.a
        )
    }
}

@Copy
class Child(
    a: Int,
    val b: Int,
): Parent(a) {

    override fun copy(a: Int?): Parent {
        return copy(a = a, b = this.b)
    }

    fun copy(a: Int? = null, b: Int? = null): Child {
        return Child(
            a = a ?: this.a,
            b = b ?: this.b,
        )
    }
}
Now problem is:
Copy code
val one: Child = Child(a = 1, b = 2).copy(a = 3, b = 1)  // fine
val two: Child = Child(a = 1, b = 2).copy(a = 3)  // call is surprisingly(?) fine, BUT returns a Parent, so does not compile
(I did not use
override val a
and so, because then the parent class would not have @Copy annotation - because no constructor parameters. And therefore this discussion would be meaningless in such case) Also another small problem you can probably see is,
: Parent(a)
← here you don't have guaranteed that the parameter is passed to correct parameter upwards, and that they mean the same thing in the domain.
p

PHondogo

04/27/2021, 11:00 AM
Overriden function can have return type of current class. In your case
Copy code
override fun copy(a: Int?): Child {
        return copy(a = a, b = this.b)
    }
r

Roukanken

04/27/2021, 11:02 AM
wait, can it? ...don't think I knew that 🤔 blame it on Java?
t

thana

04/27/2021, 11:05 AM
Yes that's java, or rather, LSP: when overriding a method you can always widen the types of arguments to a supertype (so expect the parameter to
Any
instead of
String
) and narrow the return type to some subtype
p

PHondogo

04/27/2021, 11:09 AM
So if you know at compile time that you copy Child, you'll have compile time Child as return type.
r

Roukanken

04/27/2021, 11:09 AM
okay I think you stripped me out of every argument, beyond that iffy
Parent(a)
constructor call, which could be probs solved in some iffy way (I mean I knew LSP should technically allow it, but uh... I don't think I seen this anywhere till now, and I had problems in work that needed it 🤔. Well you learn something new everyday)
p

PHondogo

04/27/2021, 11:19 AM
Semanticaly, we can use any copy(a) child or parent. But from compiler point of view it must be disambigiouty.
r

Roukanken

04/27/2021, 11:23 AM
compiler simply seems to choose the one with less parameters I thought that would be problem too, but it runs like this so... (after LSP fix) the only real problem left would be, you can't really stop smth like this:
Copy code
@Copy
open class Parent(
    val a: Int
)

@Copy
class Child(
    a: Int,
    val b: Int,
): Parent(a * b)
unless you poke into compiler really hard or smth...
p

PHondogo

04/27/2021, 11:31 AM
I don't see any problem here. Computation is made when constructing object and is not meant that this relation must be kept. It must be developer's headache to keep desired logic.
Also for current situation it doesn't metter what copy function compiler will choose.
r

Roukanken

04/27/2021, 11:37 AM
well the problem is
Copy code
var x: Parent = Child(..., 2)
// Later ...
x.copy(a = 2).a == 4  // if X was unchanged
x.copy(a = 2).a == 2  // if someone overwrote it with Parent(...)
yes, it's developers fault, but compiler should stop him from this imho
p

PHondogo

04/27/2021, 11:46 AM
Copy code
x.copy(a = 2).a == 2 // why it must be 4?
r

Roukanken

04/27/2021, 11:48 AM
because
x == Child(..., 2)
then
copy(a = q)
will call
Child(a = q, b = 2)
which then calls
Parent(a = q * 2)
==
Parent(a = 4)
basically the conclusion is, that in this case, you have no idea what
a
will be, after you set it to
2
with
copy
- because it depends on what exactly
x
is... (parent? child? and if child, what is b?) and this is pretty stupid situation and breaks whole point of
copy
method, which is why I'm for "this must be rejected at compile time"
p

PHondogo

04/27/2021, 12:01 PM
Best way is to generate noarg constructor and assign fields in copy method. Ofcourse should be done by compiler.
Idealy, compiler should only allocate memory for object and assign fields from source object or parameters.
5 Views