[HN Gopher] Kotlin Data Classes 101: Understanding Syntax, Usage...
       ___________________________________________________________________
        
       Kotlin Data Classes 101: Understanding Syntax, Usage and
       Inheritance
        
       Author : hardik_sachan
       Score  : 54 points
       Date   : 2023-01-28 10:27 UTC (12 hours ago)
        
 (HTM) web link (hardiksachan.com)
 (TXT) w3m dump (hardiksachan.com)
        
       | jillesvangurp wrote:
       | The article does not actually explain how to do inheritance. In
       | case you are wondering, the answer is using sealed classes.
       | 
       | Some neat things with data clases (vs. e.g. Java's records):
       | 
       | - default values for parameters
       | 
       | - if you use vals, your data classes are immutable
       | 
       | - copy constructor that has the same parameters as the
       | constructor with their current values as the default value.
       | 
       | - destructuring values
       | 
       | - extension and delegated properties. Those are not limited to
       | data classes of course but they are really nice to use with data
       | classes.
       | 
       | Kotlin multi platform and data classes are actually a nice
       | combination btw. We have a spring boot project with a Kotlin
       | multi platform API client that we can consume on Android, IOS, in
       | a Browser and in our integration tests for the server. Exact same
       | code working exactly the same way on IOS native, in a browser, or
       | on Android and the JVM. It uses ktor client for http
       | communication, and kotlinx serialization for json parsing. Works
       | great.
        
         | pron wrote:
         | 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 wrote:
           | Like 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 wrote:
             | > 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 wrote:
               | >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 wrote:
               | That 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 wrote:
               | If 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 wrote:
               | OCaml 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.
        
               | hota_mazi wrote:
               | All you're saying is that records are more limited than
               | data classes.
               | 
               | You see that as a strength, I see it as a restriction
               | because sometimes, I do like the extra flexibility of
               | adding mutable state to my value objects.
               | 
               | And I know better than altering the value of hashcode
               | after creation, although this only matters if you've put
               | these objects in a hashing container, so it's not even a
               | universal risk.
        
               | setr wrote:
               | Enums are also more limited than classes, yet they have
               | purpose/place. In fact, that's the general purpose of
               | types -- to reduce capabilities to gain access to
               | guaranteed capabilities, which lets you reach compiler-
               | verified logic rather than simply programmer-verified /
               | unit-test-verified logic.
               | 
               | Of course, it only matters if something utilities those
               | guarantees, which I don't know for kotlin, but C# uses
               | records to expand out the LINQ interface quite
               | substantially/conveniently for value comparison (mainly
               | made convenient when you also have anonymous records)
        
               | pron wrote:
               | I'm saying that records are a semantic features, while
               | data classes are not.
               | 
               | In the context of records, "the extra flexibility of
               | adding mutable state" makes as much sense as saying that
               | sometimes I like the flexibility of adding new instances
               | to my enum classes on the fly. An enum is an enum to
               | guarantee a fixed set of instances in the type.
               | 
               | That records and enums are more constrained in their
               | behaviour than ordinary classes is the reason for their
               | existence, because they don't do anything an object of a
               | regular class can't do. Data classes, on the other hand,
               | exist for syntactic brevity. The difference between the
               | features is not a difference in degree but a difference
               | in kind.
        
               | scns wrote:
               | Thank your for these clear explantions and showing how
               | problematic mutability can be there. As my siblings
               | allready stated this is easily mitigated but still good
               | to know.
        
           | tadfisher wrote:
           | It'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 wrote:
           | One 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.
        
           | jillesvangurp wrote:
           | I'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 wrote:
             | > 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.
        
               | hota_mazi wrote:
               | Your characterization of "hidden state" doesn't make a
               | lot of sense.
               | 
               | Either a state is part of the object's identity, and it's
               | serialized, or it's not, and it's not. This observation
               | has nothing to do with data classes, records, Java, or
               | Kotlin.
               | 
               | > The result will be equal in the `equals` sense, but the
               | resulting object will not be substitutable for the
               | original.
               | 
               | This doesn't make sense either. You are describing
               | someone who implemented flawed serialization. It's a
               | programmer error, not a language or semantic one.
               | 
               | I see very little difference of practical importance
               | between Java's records and Kotlin's value classe, and as
               | much as I value your technical writings, I am concerned
               | that your employer is tainting your objectivity when
               | trying to compare Java and Kotlin. You can do better.
        
               | pron wrote:
               | What's serialised is not identity but state. If any part
               | of the state is hidden, i.e. serialization needs to set
               | it not via the object's public API, then serialization
               | can break the object's invariants. This is what
               | serialization vulnerabilities are. You certainly _can_
               | write classes with safe serialization, but serialization
               | vulnerabilities happen because sometimes people don 't.
               | 
               | Sure, it's an error, but it is one of the errors that
               | enums and records were created to avoid. But it is true
               | that we can live without records and enums, and, indeed,
               | we did, so I wouldn't say they're critical features.
               | Still, I think it's important that people know that
               | records and enums -- unlike data classes, which are
               | syntax sugar -- were added for the guarantees they make
               | regarding certain behaviours, including interaction with
               | collections and with serialization (of any kind).
        
               | wiseowise wrote:
               | > 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.
               | 
               | If everything declared val in Kotlin, wouldn't that be
               | enough?
        
               | pron wrote:
               | Sure, just as any immutable object is fine in this
               | regard. But the point is not the fact that we can write
               | some classes that are well-behaved in certain respects,
               | but that we can reify a concept as construct known to
               | provide those guarantees. That's exactly what enums and
               | records do, but data classes don't.
               | 
               | You could always code regular classes with behaviour
               | similar to that of enums or records, but the point of
               | enums and records is that all enums and all records are
               | known to have their respective kind of behaviour.
        
         | Xeophon wrote:
         | Kotlin is simply amazing if you pair it with the corresponding
         | frameworks, i.e. kotlinx serialization instead of gson/jackson
         | etc. Quickly fell in love with the language and Java, even in
         | the new(er) versions, feels outdated.
        
         | elijahmoses wrote:
         | [dead]
        
       | Kukumber wrote:
       | I like kotlin a lot, a very well crafted language, syntax is
       | super clean
       | 
       | I wish they'd put WAY more effort into kotlin native
        
       ___________________________________________________________________
       (page generated 2023-01-28 23:01 UTC)