[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)