A very interesting read: <https://nipafx.dev/java-...
# announcements
d
A very interesting read: https://nipafx.dev/java-record-semantics. What do you guys think? Do you think data classes have a fundamental flaw? Or maybe JetBrains have something else in mind?
r
There is actually an answer to this (slowly) unfolding in Kotlin: the
value
classes, which are exactly same semantically as records - their values are only defined by their constructor parameters Yes, currently you can only have one field in them, but afaik there is plan to get rid of this restriction, which would turn them semantically same as Java records
👍 3
d
Yes, I knew I saw something about that, but I wasn't sure
n
I don't find the article very persuasive to be honest
A lot of the time you don't need any "hidden" fields and that's great, but sometimes you do, and when you do it's nice to have the option
Immutability is good too, but always-on immutability is IMHO only good if you have very good syntax for "fake" mutation, otherwise modifications in nested classes are horrific
The real fatal flaw with kotlin dataclasses is that you can't really have any invariant in them, because of the
copy
method.
I don't know if records improve on this aspect at all (and it's far more important than any notions of a "pure" product type)
c
The real fatal flaw with kotlin dataclasses is that you can’t really have any invariant in them, because of the 
copy
 method.
can you give an example for that?
n
Well, .copy just lets you modify any field individually right? So whatever invariant you verified in the constructor (or is true by construction) can be destroyed by just changing an individual field
You could have a dataclass that holds a non-empty string; its public constructor verifies that a passed string is non-empty, otherwise it throws
but you can just use
.copy
to create an instance of your dataclass with an empy string
c
hmm i wonder why does the copy method not just call the constructor?
n
Copy lets you change just one field so I'm not sure that it could call the constructor
z
Jake has written about some issues with data classes - I thought Java’s solution avoided most of these
c
but the copy method is basically like a constructor so it would be less suprising if it just called the constructor.
n
Yeah, i was looking at how python dataclass handled it , and python dataclass' do indeed call their constructor (init function) in their equivalent of copy (replace)
@Zach Klippenstein (he/him) [MOD] i don't see discussion about this particular issue in that link from a quick skim but maybe I added it
*maybe I missed it
r
Wait, copy does not call constructor? Suddenly my opinion of data classes is waaay lower...
n
Pretty sure it does not, no
r
That was kidna my default assumption...
c
mine too.
n
Another interesting option is that in python, dataclasses can declare a post init function. If it's declared, it's called. The post init function could verify invariants and throw. And it could be called both after construction and after copy.
c
good that its not true 🙂
Copy code
data class A(val name:String) {
    init {
        if (name != "blah")
            throw RuntimeException("must be blah")
    }
}

val a = A("blah")
val c = a.copy("bleh")
println(c.name)
this throws
n
it looks like init blocks are called
but probably code inside individual constructors is not
c
Copy code
ata class A(val name:String) {
    val b = kotlin.run {
        if (name != "blah")
            throw RuntimeException("must be blah")
    }
}

val a = A("blah")
val c = a.copy("bleh")
println(c.name)
fails too
n
So I guess, it works out if you put your validation in an init block, but if your invariant is a result of the logic of an individual constructor you're still out of lock
*luck
c
so it seems perfectly possible to guarantee that a data class is always valid
except of course reflection
a
How does one deconstruct a kotlin data class? Lack of that capability is detrimental for what wants to be a product type.
d
after decompiling the bytecode from this:
Copy code
data class A(val str: String)
I get:
Copy code
public final class A {
   @NotNull
   private final String str;

   @NotNull
   public final String getStr() {
      return this.str;
   }

   public A(@NotNull String str) {
      Intrinsics.checkNotNullParameter(str, "str");
      super();
      this.str = str;
   }

 ...
   @NotNull
   public final A copy(@NotNull String str) {
      Intrinsics.checkNotNullParameter(str, "str");
      return new A(str); // Calling constructor
   }

   // $FF: synthetic method
   public static A copy$default(A var0, String var1, int var2, Object var3) {
      if ((var2 & 1) != 0) {
         var1 = var0.str;
      }

      return var0.copy(var1);
   }
...
}
So I don't get why @Nir said it doesn't call the constructor. It's also not true that
copy
allow s you to change just one property. You can change as many as you want of them.
Copy code
data class A(val str: String, val str2: String)

fun foo() {
    val a = A("1", "2")
    a.copy(str = "a", str2 = "b")
}
r
thank god, I got a scare 😅 and @Nir, if by those invariants, you mean secondary constructors (the ones you have to declare manually inside body), then yes, those can't be verified - since that means you already have at least 2 constructors (primary - init blocks/val declarations/etc, and secondary - manually declared one) and you can't really guarantee with compiler that two pieces of code produce same invariant - that's on programmer.
1
d
About the always on immutability - what I suspect is that JetBrains did indeed have different purpose for
data classes
and the
@JVMRecords
annotation is just for interop (as mentioned in the article). I see lack of explicitness if
data classes
are intended to be used often as records, so I do hope that
value classes
will manage to serve that purpose
though it sounds a bit inconsistent to have a
value class
inlined
when used with just one property, but to act as
records
when having multiple properties
r
imho, the inlining can work even with multiple properties like when you look at it, there is nothing really stopping it
n
@christophsturm this only works for invariants that you manually verify in the init and throw, which is just one technique via which invariants can be enforced
Copy code
data class foo private constructor(val x: String) {   
    constructor(y: Int) : this(y.toString()) 
}
foo
has the invariant that it always holds a string that is the representation of an integer
copy lets you break this
c
if you make the primary constructor private you must make the copy method private too.
d
The copy method is auto-generated so you can't make it private
2
n
In an example like this, you see what I mean by the constructor is not really called. Yes, the default constructor is called, but in this case it's not the one you want/care about.
r
well the other problem is, that copy has no way of calling the secondary constructor
c
i think its just not true. the jvm just mandates that the primary constructor is always called. and thats exactly what the copy method does
n
Yes, the default constructor is called, but in this case it's not the one you want/care about.
Anyway you slice it, it's a problem for encapsulation that the copy function cannot in standard kotlin be made private or omitted or replaced
c
you can just remove the date keyword 🙂
n
and then write hash and equals by hand
c
a data class does not really do much except copy constructor and hash and equals
r
imho, the copy method should mirror the visibility of primary constructor that's the only real solution to this
n
That's a decent solution though backwards compatibility makes me wonder if it's possible
c
this is all so much less bad than the initial “copy method does not call constructor” claim
r
you can't autogenerate copy methods for secondary constructors, (for example, calling
.copy()
on your class has no way to know how to call the secondary one with just
String
) calling copy from not private environment, on classes with primary constructor private is basically madness
d
it isn't very backwards compatible, yes.. I'd also agree that perhaps the copy method should mirror the primary constructor visibility
n
@christophsturm agree to disagree
c
i think idea should issue a warning when you make the primary constructor of a data class private
r
it does actually
image.png
c
yeah
just after offering to make the constructor private:
n
it's a band aid. Ultimately you're still left out writing hash and equals by hand for certain kinds of data classes you'd want to right, which rather defeats the purpose
c
for how many data classes did you have to do it already?
n
I already hit this issue writing trivial code for advent of code 🙂
If you think in terms of class invariants, you hit this issue a lot
But that's not a very emphasized point in Java class design, IME (which is probably why this mistake was made to begin with)
c
i mean if you have a string that is a number, maybe you want a custom hash method too
n
maybe, but that's up to the user. And real life examples obviously tend to not be this trivial.
r
pretty sure I did hit it while AoC too I didn't use copy ofc, but if I was writing API that code wouldn't be acceptable
n
You'll basically have this problem every single time you want to have a private primary constructor, and create your individual fields from something other than their exact 1-1 to mapping. For example, you could have a dataclass that stores a name and phone number, but its public, secondary constructor takes a name and a database object
It's really nice, since you're creating a new type, if this new type gives you the guarantee that the phone number is the one that goes with the name, according to your database.
I find these examples pretty common and just falling back to writing hash and equals by hand each time is bad. Worse, a lot of people will be too lazy to do that, and most likely they'll just leave the "copy loophole" present in the code. Which means silently broken invariants.
c
hmm you’re right. making the copy method get its visibility from the default constructor would be the perfect solution
maybe for #CQ3GFJTU1?
n
Provided you could still write your own copy method, which I think would be ok because of overloading
Other than that one aspect, I agree it would be ideal
r
overloading should be able to handle the own copy ye you can do it even rn, just in some cases you would need to do stuff like
Copy code
data class Foo private constructor(val x: String) {
    constructor(y: Int) : this(y.toString())

    public fun copy(y: Int? = null): Foo = Foo(
        y = y ?: this.x.toInt()
    )
}

Foo(4).copy(y = null)
(which the
private
on the other one, should be able to handle)
there is some issue on this topic https://youtrack.jetbrains.com/issue/KT-11914