Would it make sense to introduce native builder functionality to data classes? Often, there's a use ...
d
Would it make sense to introduce native builder functionality to data classes? Often, there's a use case where two or more components build the same data class where most of the fields get the same value while only some field values are specific to each component. I don't have a concrete proposal but it could look like this:
Copy code
data class X(a: Int, b: Int, c: Int)

val xBuilder = X.builder(a = 1, b = 2) // type X.Builder ?
xBuilder.build(c=3) // type X
AFAIK there's no nice way how to implement the described use case without code duplication if you don't have control over the data class.
h
Not sure about the "often" part – I've never needed it… 😅 If you have multiple instances where most components are the same, you could use `copy`:
Copy code
val builder = X(23,42,999)
    val x1 = builder.copy(c = 1)
    val x2 = builder.copy(c = 2)
    val x3 = builder.copy(c = 3)
☝️ 2
d
Yeah, I've also considered that but using a throw-away default (which of course is not a simple int in reality) sounds like a hack...
e
I made a PoC ages ago, https://github.com/ephemient/builder-generator (unmaintained, but you can make your own pretty easily)
d
Thanks. Apart from solving a concrete problem on my side I also wanted to open a discussion about whether something like that shouldn't be added to the language. If we have generated toString, etc. why not also builders?
r
Not commenting on the overall language issue but another way to solve the concrete case is to abstract the common fields into another data class, and have that be a property of the more specific classes. You did say you don't have control over the data class though.
b
🤭 so many people trying to solve the same problem I did a PoC ages ago as well 😄 https://github.com/blipinsk/data-class-builder
d
Yeah, that was exactly my thinking that people must be somehow trying to solve the problem so it might be a better idea to integrate it into the language natively.
e
I don't think it's a very common need. Java's builder pattern isn't used in Kotlin. there is a different sort of builder pattern (in a lambda) used by libraries concerned about ABI stability through API evolution, but in that case they're written by hand - having control over the ABI is important
d
To be clear, my inspiration isn't the Java builder pattern. I'm actually a Kotlin-only dev 🙂 and most of the time I'm satisfied with the standard Kotlin idioms. Here I'm just wondering if there's anything principally wrong if the compiler created data class builders. I think that the use case is quite clear and the current solution (write the builders manually for each case) is unnecessarily verbose.
r
The solution would normally be to use composition for your data classes. It only helps the limited case in which the data class can't be structured that way for some reason, in which case the verbosity isn't terrible. Generally having multiple ways to do the same thing isn't great.
e
users should just use your data class constructors in normal Kotlin code. in cases where it's a library that will evolve and you need to maintain ABI compatibility, then
data class
is the wrong tool anyway https://jakewharton.com/public-api-challenges-in-kotlin/
d
In my case, the data classes are actually generated from an OpenAPI spec. Not sure what I can do better/differently here.
r
Generate OpenAPI from code-first annotations, and then you can structure your data classes exactly how you want them. SpringDoc does this for Spring apps. Kompendium is a good option for ktor.
If its not your own spec then yeah, different story
e
write your own openapi code generator, we've customized ours in myriad ways and I don't know of anybody that likes the default
b
The fundamental problem with java-style builder in kotlin (and the reason why I abandoned the PoC I built) is the fact that the validation happens in runtime. Kotlin with it's default values for constructor arguments is just a less prone to errors. It's just not worth it.
👍 1
d
Generate OpenAPI from code-first annotations
Yeah, the spec is not under my control, unfortunately. Still, can I generate a flat API model from a nested data class? (I've not read the docs about this generator part yet)
The fundamental problem with java-style builder in kotlin
Yeah, if you write your own then surely there will be problems. That's why I'm asking whether it wouldn't be a good idea to incorporate the feature into the language. I mean, if we already have
copy
why not builders?
e
how would that fix anything? you could still end up with
Copy code
X.builder().build()
blowing up at runtime
d
Ok, I probably don't understand what exactly you mean...
b
Yeah, if you write your own then surely there will be problems. That’s why I’m asking whether it wouldn’t be a good idea to incorporate the feature into the language. I mean, if we already have
copy
why not builders?
It’s a problem with the pattern, not with building the pattern outside of the default language features. The problem is this:
Copy code
data class Shape(val width:Int, val height:Int)

val builder = Shape.builder(width = 100)
builder.build() // nothing prevents you from calling this even though you did not provide `height`
you could only check if
build
can produce a valid instance in runtime.
so essentially you’d create a code which compiles file, but crashes in runtime. In a trivial example like
Shape
☝🏻 you could create smarter builder class tree in which you don’t have
build
function unless you set both arguments, but it doesn’t scale past a couple of arguments. Simply not worth it.
e
Copy code
data class Shape(val width: Int, val height: Int) {
    class Builder {
        fun width(width: Int) = BuilderWithWidth(width)
        fun height(height: Int) = BuilderWithHeight(height)
    }
    class BuilderWithWidth(val width: Int) {
        fun width(width: Int) = BuilderWithWidth(width)
        fun height(height: Int) = BuilderWithWidthAndHeight(width, height)
    }
    class BuilderWithHeight(val height: Int) {
        fun width(width: Int) = BuilderWithWidthAndHeight(width, height)
        fun height(height: Int) = BuilderWithHeight(height)
    }
    class BuilderWithWidthAndHeight(val width: Int, val height: Int) {
        fun width(width: Int) = BuilderWithWidthAndHeight(width, height)
        fun height(height: Int) = BuilderWithWidthAndHeight(width, height)
        fun build() = Shape(width, height)
    }
}
"safe" builders are combinatorial explosion and doesn't scale whether it's written by hand or generated
☝️ 1
d
Got it. Thanks.