I've been playing around with a design for immutable objects and builders and I've run into a couple of problems. I'm aiming for this:
val foo1 = Foo(x = 1, y = 2) // Foo is immutable
val foo2 = foo1.copy { y = 3 } // creates a new instance with x = 1, y = 3
The copy function:
- creates a builder object from foo1
- executes the closure (an extension function on the builder) to update the builder’s state
- builds a new instance of Foo from the builder.
I’ve got something that almost works. The code is below. But I’ve run into a couple of problems:
- I have to specify the type parameters for the copy function at the call site which messes up the syntax. Will the compiler be able to infer these at some point?
- I have to specify the bounds of the builder type in 2 places, the declaration of Copyable and the declaration of copy. Should the compiler be able to infer the bounds in the declaration of copy?
- If I change the order of the supertypes in the declaration of Bar or BarBuilder it doesn’t compile. The compiler thinks the class doesn’t extend the trait. I assume that’s a bug.
I’m using build 0.1.3299 of the plugin.
// T is the immutable type
trait Builder<out T> {
fun build(): T
}
// T is the immutable type, U is the builder
trait Copyable<out T, out U : Builder<T>> {
fun builder(): U
}
open class Foo(val x: Int, val y: Int) : Copyable<Foo, Foo.FooBuilder> {
override fun builder(): FooBuilder = FooBuilder(x, y)
open fun toString(): String = “Foo[x=$x, y=$y]”
open class FooBuilder(var x: Int, var y: Int): Builder<Foo> {
override fun build(): Foo = Foo(x, y)
}
}
class Bar(x: Int, y: Int, val z: Int) : Copyable<Bar, Bar.BarBuilder>, Foo(x, y) {
override fun builder(): BarBuilder = BarBuilder(x, y, z)
override fun toString(): String = “Bar[x=$x, y=$y, z=$z]”
class BarBuilder(x: Int, y: Int, var z: Int) : Builder<Bar>, Foo.FooBuilder(x, y) {
override fun build(): Bar = Bar(x, y, z)
}
}
fun <T : Copyable<T, U>, U : Builder<T>> T.copy(fn: U.() -> Unit): T {
val builder = this.builder()
builder.fn()
return builder.build()
}
fun main(args : Array<String>) {
val foo1 = Foo(x = 1, y = 2)
//val foo2 = foo1.copy { y = 3 } // this doesn’t work
val foo2 = foo1.copy<Foo, Foo.FooBuilder> { y = 3 } // this is ugly
val bar1 = Bar(x = 1, y = 2, z = 3)
//val bar2 = bar1.copy { z = 23; y = 21 } // this doesn’t work
val bar2 = bar1.copy<Bar, Bar.BarBuilder> { z = 23; y = 21 } // this is ugly
println(foo1) // Foo[x=1, y=2]
println(foo2) // Foo[x=1, y=3]
println(bar1) // Bar[x=1, y=2, z=3]
println(bar2) // Bar[x=1, y=21, z=23]
}