5 ms·
The reason Java rejected data classes in favour of the very different and more powerful approach of nominal tuples (i.e. records) and algebraic data types in ge
by pron 4y ago
The reason Java rejected data classes in favour of the very different and more powerful approach of nominal tuples (i.e. records) and algebraic data types in general, was the observation that the vast majority of data classes can be record classes. It's by not trying to cover the remaining small minority of cases with the same construct that we gain the guarantees of a more powerful one based on the property that the full state of a record is described by its canonical constructor and its deconstruction serves as its dual.
All records are well-behaved as collection members; not all data classes are.
All records can be safely serialised and deserialised; not all data classes can.
All records can be deconstructed and reconstructed; not all data classes can.
Data classes are about declaring classes with less code. When you have an object of a data class you know nothing more about its behaviour than you do about an object of any class. Record classes, which are similar in their design philosophy to enums, are not about writing less code, but about being reifying the concept of unencapsulated data, which offers semantic guarantees -- not just less syntax -- about the nature of the objects, just as you can with enums.
Java is able to offer new constructs such as records and enums, with runtime support, as it's not a hosted language.
- scns 4y agoLike your input a lot. > All records are well-behaved as collection members; not all data classes are. > All records can be safely serialised and deserialised; not all data classes can. > All records can be deconstructed and reconstructed; not all data classes can. Can you give examples please?
- pron 4y ago> All records are well-behaved as collection members; not all data classes are. This is due to immutability. A var in a data class constructor means that the object's hashCode may change after its construction, and so mutating such an object may break collections that rely on hashCode to locate members, such as HashMaps and HashSets. > All records can be safely serialised and deserialised; not all data classes can. This is because records do not have any state beyond that which is constructed by the canonical constructor, which is never bypassed. When serializing a record, its components are serialized. When deserializing one, the only way to construct the record is by passing the deserialized components to the canonical construct, which then validates them. Deserializing an object with hidden state bypasses the constructor and can inject inconsistent, potentially harmful, data into it. > All records can be deconstructed and reconstructed; not all data classes can. A record is its components (and type; this is why they're nominal tuples). When you deconstruct a record with a pattern, you get its full state. If you put it back together you'll get an object with the same data as the original.
- ohgodplsno 4y ago>A var in a data class constructor Just because you can, doesn't mean you should. I can count on one hand the number of data classes with vars in our codebase of 500k+LOC, and they are specialized implementations for processes that would otherwise entail lots of copying and awful performance, whereas mutation just works.
- pron 4y agoThat is the exact observation I was referring to! You can make that desired behaviour into a guarantee that the runtime and any other code can rely on without giving up much. So even though most data classes could be records, the construct still doesn't make the necessary guarantees. Because those times where data objects require mutation are not numerous, the benefit of reducing their syntax is small. So Java was able to give the syntax benefits in almost all of the same cases, and additionally it was able to make powerful behaviour guarantees.
- Tainnor 4y agoIf you follow that logic to its conclusion, it means that you should just replace your Java code with Haskell (or at least OCaml). These are fine languages, but Kotlin is known to be more pragmatic. There are some footguns, make sure to be aware of them.
- pron 4y agoOCaml and Haskell are very different; OCaml is much closer to Java than it is to Haskell. Java is, or soon will be, a nominally-typed ML-ish (which isn't saying all that much given how many languages were inspired by ML). You could say that one difference between Haskell and OCaml/Java is that OCaml and Java have algebraic data types, while Haskell has only algebraic data types. The only way my logic could lead to the conclusion that the Haskell approach is always advisable is if you mistake "ADTs are very useful" to mean "anything beyond ADTs is harmful", which is something that Java or OCaml don't say.
- jillesvangurp 4y agoI'm sure there are good reasons for why record classes are the way they are. But the reasons you cite strike me as a bit esotheric / low value. Serialization works fine with kotlinx.serialization. Java's built in serialization is a bit of a relic of the past that doesn't really have that many use cases left. I'll write that off as a "I couldn't care less". I've actually used java serialization but not this century. There probably is still some legacy stuff around that depends on it but nothing a typical Kotlin user would care about. As for collections. Not sure what you mean here. The Java collections take just about any object. Including data class objects. You need working hashcode and equals implementations. Which are provided by data classes. There are some limitations with deep comparisons particularly with array types. The Kotlin compiler will warn you about those. Am I missing something else here? Constructing/destructing seems to work fine for me. Nice feature. Copy constructors in particular are neat. Am I missing out on something here? You make it sound that this is kind of a big deal or a big compromise but I just don't see it.
- pron 4y ago> I've actually used java serialization but not this century. I'm not talking about Java's built-in serialization but any kind of serialization. An object with hidden state cannot be safely deserialised regardless of mechanism. Hidden state, by definition, means that its consistency (which may translate to security) is not validated by mechanisms that are known to the deserialisation mechanism. Safe serialisation requires that there be no hidden state, i.e. that all of the objects state is validated by public mechanisms. > You need working hashcode and equals implementations. Which are provided by data classes. No, that's not enough. Some collections (such as hash maps and has sets) additionally require that the hashCode does not change over the object's lifetime, or at least over the time the object is in the collection. > Constructing/destructing seems to work fine for me. Reconstruction can only work when all of the object's state is deconstructed. That's not the case for data classes. The result will be equal in the `equals` sense, but the resulting object will not be substitutable for the original. Equal records are substitutable (unless you depend on object identity, but object identity is the focus of another project -- Valhalla) > but I just don't see it. The powerful properties of algebraic data types have been studied for half a century now, and there's a lot of literature you can find on the subject. That's not to say that data classes aren't helpful, but they only address the problem of syntax ceremony. Records, by focusing on semantics, not only on syntax, additionally address other important problems.
- tadfisher 4y agoIt's important to note that `data class` works on JVM platforms before version 16, and the Kotlin compiler will check the additional constraints and generate records if you add the `@JvmRecord` annotation. This is a reasonable approach for a non-Java JVM language, especially considering records is an ABI- and API-incompatible addition.
- sixbrx 4y agoOne downside of Java records seems to be that they can't participate as the alternatives (subtypes) for a sealed abstract type, since they can't extend any classes other than Object. Ie. the very common "sum of product types" pattern can't use records as the product types - which they would have been great for! You can have normal wrapper classes as the subtypes of the sealed type, and have those wrappers use records internally, but all the brevity and clarity is lost that way IMO and it just becomes obfuscation.
- pron 4y agoOf course they can! They can implement interfaces, which are sealed. In fact, that's one of their most common usages: sealed interface Option { record InputFile(Path path) implements Option {} record OutputFile(Path path) implements Option {} record MaxLines(int maxLines) implements Option {} record PrintLineNumbers() implements Option {} } (see https://www.infoq.com/articles/data-oriented-programming-java/ https://www.infoq.com/articles/data-oriented-programming-jav...) The problem of extending classes is that it can introduce hidden state and also result in non-reflexive equality. But interfaces are fine.
- sixbrx 4y agoThanks for that, I knew I must have been missing something!