Would it be possible to have data interfaces? ```...
# language-proposals
d
Would it be possible to have data interfaces?
Copy code
data interface GenericEntity(
  val id: String,
  val created: Instant,
)

data class Customer(
  val name: String,
): GenericEntity
I suppose the syntax I propose here isn't the best... Anyway, the idea would be to have the interface allow usage of copy methods as if it was a data class with just those values and ease up reuse of common data structures. Currently there's no way to rely on the copy and other helpers methods of data classes unless you expose the implementation.
a
What implementation would be created when calling the copy method on the interface?
d
The same class of the copied instance
👍 1
j
I actually think the idea of a
data interface
is a decent idea, but as a wholesale replacement for
data class
(which should be deprecated and fired into the sun). It would describe the values that a type can provide and would automatically enable copy-like semantics (but not through problematic generation of a single function) and nominal destructuring (not positional). Basically, it would be a descriptor on top of which those two features (copy-like withers and nominal destructuring) could be built.
👍 1
👍🏾 1
j
if you deprecate
data class
, a
data interface
can be easily instantiated without extending another class? if not you can have a few boilerplate
Copy code
data interface Foo(val foo: String)

fun main() {
    // should work, if not it would be annoying
    Foo("hi")
}
d
I don't know exactly what to point to that I dislike of data classes... I'll try anyway. I feel like I'd love to have a way to just say "add copy and equals and toString automatically to this class" without having to use a data class. If I want to keep a data class as an implementation detail and use an interface to expose only what I need to expose I need to write a lot of boilerplate or resort to KSP / KAPT to generate the code. I think data classes features are nice to have but I'd like to have more control • A way to tell the compiler to generate copy methods etc (data interface?) • And exactly of which fields I want to expose a copy of • Possibly the ability to override the copy method with my own logic Data classes are a bit limiting while being convenient to most use cases. I'm sorry if I can't express what I'd want in a better way. I still didn't manage to focus exactly on what feels wrong.
👍 1
👍🏾 1
d
I'd like to see the entire concept of a
data class
be decomposed into more flexible ways of defining methods based on any given class's properties. For example, suppose we had the
data class
toString
implemented like this:
Copy code
reified fun <reified T : Any> T.customToString(): String = T::class.memberProperties.joinToString(
    separator = ",",
    prefix = T::class.simpleName + "(",
    postfix = ")",
) {
    it.name + "=" + it.get(this)
}
(Here,
reified fun
would mean that the method is generated once per class, and the
memberProperties
loop is unrolled at compilation time, no runtime reflection necessary.) Then, in any class, you could declare:
Copy code
override fun toString() = customToString()
Or, one could declare that functionality in an annotation, if lambdas were made to be acceptable annotation parameters:
Copy code
@OverrideToString(::customToString)
and then if you allow for annotation aliasing:
Copy code
alias @DataToString = @OverrideToString(::customToString)
and then a combination:
Copy code
alias @DataClass = @DataToString + @DataEqualsHashCode + @DataCopy + @DataComponentN
and suddenly
@DataClass
is equivalent to adding
data
. Main use case that I encounter would be to allow for easily creating an equivalent of
data class
that will use
contentEquals
,
contentHashCode
, and
contentToString
for arrays (or at least respect yet another annotation on individual properties that define alternative definitions, like
@DataArray val x: IntArray
with
alias @DataArray = @DataArrayToString + ...
and
alias @DataArrayToString = @DataToString(::contentToString)
) so that adding a single array to a data class doesn't require abandoning the concept of a data class altogether.
nono 2
v
Sounds like re-inventing lombok
🤮 3
plus1 2
e
> What implementation would be created when calling the copy method on the interface?
The same class of the copied instance (edited)
that's not a constraint that is compatible with Java's type system
d
data class
was already largely re-inventing Lombok, except that Lombok handled arrays using
Arrays.deep...
methods while data classes have no special handling for them. Lombok also only had control on the individual fields by allowing for opt-out or opt-in, with no further customization. In both cases, if you have a single field in which you want its string value in the class's
toString
to not match the class itself's
toString
(such as with arrays, or if you want some truncation), you must abandon the entire concept and generate your
toString
entirely manually, with all the code bloat and risks of not updating that come with it. (To be clear, I'm not entirely attached to having annotations that add functions to classes, though it would require some new syntax to ensure that multiple methods can be generated together, particularly important for
equals
and
hashCode
.)
👍 1
e
Our current thinking about future evolution of data classes is to gradually replace them with
value class
concept that is just like data class, but readonly, identityless, and without positional destructuring. Since
copy
methods are harmful for library evolution, they’ll be replaced with evolution-friendly
copy var
concept. The
data interface
that you want will be available as
value interface
. See this document for full details on this vision: https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md
👍 1
a
One of the use cases of the
copy
method is copying the state in reducer-like functions:
fun reduce(state: State, msg: Msg) : State
I really hope this use case will be supported by value classes as well, if they are going to replace data classes. In other words, obtaining a new reference with some properties changed, and with as little boilerplate as possible.
e
j
@elizarov but
copy
returns a different instance of the state
Copy code
fun addTag(tag: String) {
    state.lastUpdate = now()
    state.tags += tag
    notifyOnChange(state)    
}
here, where is the new instance? If the new instance comes from the operation with that sugar, then we would need:
Copy code
fun addTag(tag: String) {
    val stateUpdated = ((state.lastUpdate = now()) + (tags += tag))
    notifyOnChange(stateUpdated)    
}
If there is a local state which is changed, the sugar is not exactly the same than
copy
a
As far I understand, there is a
copy fun
concept, see here. However, I've got even more questions, asked everything here.
👍 1