0
\$\begingroup\$

I'm trying to make a safe builder using Kotlin that can be partially built and then copied, perhaps multiple times, to avoid repetition. Note that the properties that are reused aren't always the same - I've reused name and real here, but sometimes age and real or only age might be reused. This is what it looks like (Main.kt):

import builders.*

fun main() {
    val bobProto = personBuilder()
        .name("Bob")
        .real(false)

    val bobSenior = bobProto.copy()
        .age(100)
        .build()

    val bobJunior = bobProto.copy()
        .age(0)
        .build()

    println("Bob Sr. = $bobSenior, Bob Jr. = $bobJunior")
}

This code builds two instances of Person that share a couple of properties but differ in another. Person is defined in people/Person.kt as such:

package people

data class Person(val name: String, val age: Int, val real: Boolean)

The implementation of the builder and other functions is in builders/PersonBuilder.kt:

package builders

import people.Person

object SetProp
object NotSetProp

open class PersonBuilder<N, A, R> {
    var _name: String? = null
    var _age: Int? = null
    var _real: Boolean? = null
}

fun personBuilder(): PersonBuilder<NotSetProp, NotSetProp, NotSetProp> = PersonBuilder()

fun <A, R, T> T.name(name: String): PersonBuilder<SetProp, A, R> where T : PersonBuilder<NotSetProp, A, R> {
    this._name = name
    return this as PersonBuilder<SetProp, A, R>
}

fun <N, R, T> T.age(age: Int): PersonBuilder<N, SetProp, R> where T : PersonBuilder<N, NotSetProp, R> {
    this._age = age
    return this as PersonBuilder<N, SetProp, R>
}

fun <N, A, T> T.real(real: Boolean): PersonBuilder<N, A, SetProp> where T : PersonBuilder<N, A, NotSetProp> {
    this._real = real
    return this as PersonBuilder<N, A, SetProp>
}

fun <N, A, R, T : PersonBuilder<N, A, R>> T.copy(): T {
    val pb = PersonBuilder<N, A, R>()
    pb._name = this._name
    pb._age = this._age
    pb._real = this._real
    return pb as T
}

fun <T> T.build(): Person where T : PersonBuilder<SetProp, SetProp, SetProp> =
    Person(this._name!!, this._age!!, this._real!!)

This works, but because it's a bit hacky, the error message when a property is left out before calling build or when a property is set twice is cryptic - it simply says that the extension method could not be used because of a receiver type mismatch. There doesn't seem to be a way to add custom error messages, though, and Kotlin's contracts didn't seem to help here either.

Furthermore, the properties of the builder (_name, _age, and _real) have to be public for the extension functions and so can be accessed from anywhere. I've made their names start with underscores, but that doesn't keep them from being visible.

I'd like to know if there's a more idiomatic way to make a safe builder that checks that all properties are initialized exactly once at compile-time, or at least if there are any minor improvements I can make in this code.

\$\endgroup\$
5
  • \$\begingroup\$ Is the compile-time check to make sure all properties are initialized exactly once a strict requirement? What's the rationale behind this? \$\endgroup\$ Commented Jul 20, 2021 at 20:58
  • \$\begingroup\$ @SimonForsberg Having each property initialized once is pretty much the primary goal I had in mind while making this, although I could do without it if absolutely necessary, I guess. It's mostly a matter of convenience. One problem with having runtime checks is that you need to run it every time you want to check if it works. I think that's reasonable, but it's more convenient if IntelliJ just immediately highlights errors. \$\endgroup\$
    – user
    Commented Jul 20, 2021 at 21:06
  • 1
    \$\begingroup\$ This question has been discussed in chat \$\endgroup\$ Commented Jul 20, 2021 at 21:10
  • \$\begingroup\$ Is it okay if you set the properties in always the same order, similar to this question, or does the order of setting properties have to be flexible ? \$\endgroup\$ Commented Jul 20, 2021 at 21:22
  • \$\begingroup\$ @SimonForsberg Since some properties may be reused (and it's not always the same properties each time), the order should be flexible. \$\endgroup\$
    – user
    Commented Jul 20, 2021 at 22:03

2 Answers 2

4
\$\begingroup\$

The only advice I have is: Don't do that.

You're abusing generics, massively, IMO.

A data class where all the properties are val is the best compile-time validation Kotlin has of "making sure all properties are given a value exactly once".

Then make different functions to create objects from different templates.

With your Person example above that would be something like:

fun makeBob(age: Int) = Person(name = "Bob", real = false, age = age)
\$\endgroup\$
2
\$\begingroup\$

I have no problem at all with the essence of your idea (safe builders) but I very much dislike the code:

  1. It requires as many generic parameters as there are properties, practically inviting the need/desire for generic generic (not a typo) parameters or for code generation.
  2. It allows clients to decide whether a person object classifies as a person-template or a real person.

Point 1 is my main concern while point 2 is somewhat off-topic but perhaps worth mentioning.

If you really (really?) insist on compile-time verification that much, the code makes perfect sense. But if so, what makes the underlying problem so important that it it prevales over much simpler run-time solutions?

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for your answer. Your first point is valid, and I did plan on generating the code for the builder for classes with many parameters. Point 2 is an intended feature, and I'd like to keep it that way. Having it verified at compile-time is just more convenient than having to hit the run button to test if the Person object was constructed successfully. Do you have any suggestions as to make the code more "likable"? \$\endgroup\$
    – user
    Commented Jul 21, 2021 at 13:26
  • 1
    \$\begingroup\$ @user: if you intend to use code-generation there's no need at all to make the code likeable, i.e. you can fully (and justifiably) ignore any likeability-aspect (as long as the compiler is fine with it of course ;-). I don't get your "run button"-argument, and I'd still solve your problem differently, but I'll give it the benefit of the doubt since I don't know the context. \$\endgroup\$
    – Koen AIS
    Commented Jul 21, 2021 at 13:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.