CC <@U073BL2T8V8> I'm looking for some inspiration...
# opensource
p
CC @Oleg Smirnov I'm looking for some inspiration how to gracefully replace a Java-style builder with a class and named arguments. See the discussion: https://github.com/krzema12/snakeyaml-engine-kmp/pull/589#discussion_r2428970571. As one of the approaches, I'm wondering if there's a clean and safe way to have something like
.copy(...)
, but without the risks of publishing it as an API and binary incompatibilities if the signature changes. Or maybe is there yet another approach?
c
Typically you would do so using a DSL;
Copy code
.copy {
    someField = …
}
which is much easier to guarantee binary compatibility for
1
p
oh, smart! it requires creating a mutable object which is then used to instantiate the ultimate object, right?
👍 1
just recalled there's https://github.com/JavierSegoviaCordoba/kopy, but I'm not sure if it's suitable to be used to produce something that can be distributed as a part of a lib's API. It requires some. Do you have experience using it in libs' APIs?
m
kopy
focus is more on mutating nested immutable classes IIRC.
💡 4
p
@CLOVIS is this what you have in mind? does it look correct to you (the general idea, not in terms of correctness of e.g. covering all fields)?
started wondering if it's truly immune to ABI incompatibilities across versions. Thinking aloud: the
copy
function's signature will remain the same if we e.g. add more fields. The function depends on a
...Mutable
class which itself is public, but it's not intended to be used in a way other than via
copy
. Given that the impl of
copy
and the constructor's signature of
...Mutable
are in sync, things should work fine
c
Personally I would go further: make the receiver an interface with only
var
properties, this way the mutable class is not even part of your public API at all. If your immutable entity is an interface too, the mutable one can extend the immutable one. That is more private code, but a smaller API surface.
As in:
Copy code
interface MutableFoo {
    var field1: Int
    var field2: String
}

fun Foo.copy(block: MutableFoo.() -> Unit): Foo {
    val result = object : MutableFoo {
        override var field1 = this@copy.field1
        override var field2 = this@copy.field2
    }.apply(block)

    return Foo(
        field1 = result.field1,
        field2 = result.field2,
    )
}
The same interface can be used for initial construction, not just copying
p
thanks, I'll try it out!
m
I like keeping the
Builder
suffix FWIW:
Copy code
Foo.copy(block: FooBuilder.() -> Unit): Foo
👍 1
c
I don't 😅
Builders are usually fluent and immutable, this is neither
I usually go with
xxxScope
or
xxxDsl
m
Would be cool to make a list of what's used in the ecosystem today
This is the kind of stuff where convention > personal taste
👍 2
p
Here's a proposed change: https://github.com/krzema12/snakeyaml-engine-kmp/pull/592, can I ask you to review it? (comments can be in GitHub!)