Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kotlin typesafe builder to help avoid the need for constructor use #2589

Closed
cgruber opened this issue Aug 23, 2023 · 5 comments
Closed

Kotlin typesafe builder to help avoid the need for constructor use #2589

cgruber opened this issue Aug 23, 2023 · 5 comments

Comments

@cgruber
Copy link

cgruber commented Aug 23, 2023

For Kotlin gencode, we were using constructors a lot, before realizing the diamon-dependency-skew/ABI-incompatibility issues (obvious in retrospect). But we like the terseness of the syntax, and the java builder syntax is bulky and feels... not so kotlin-y.

I built a quick workaround in our services - one form that let us use kotlin typesafe builders even for java-generated protos, but it's even cleaner for kotlin generated protos (because the Companion object is available to make extension methods) (see below for example). After working with it a bit, it occurred to me that this could just be generated on the Companion object from the start, to allow for this syntax to be used if there is a Builder generated at all.

The generalized pattern is:

fun <M : Message<M, B>, B : Message.Builder<M, B>> buildProto(constructor: () -> B, initializer: B.() -> Unit): M {
  val builder = constructor.invoke()
  initializer.invoke(builder)
  return builder.build()
}

With this, one could comfortably do one-liners per builder, such as (for java, where there's no companion object to hang an extension function):

fun myProto(i: MyProto.Builder.() -> Unit) =  buildProto({ MyProto.Builder() }, i)

val myProto = myProto {
  prop1 = something
  prop2 = somethingElse
}

or for kotlin with a Companion object, the slightly more consistent and discoverable:

fun MyProto.Companion.build(i: MyProto.Builder.() -> Unit) =  buildProto({ MyProto.Builder() }, i)

val myProto = MyProto.build {
  prop1 = something
  prop2 = somethingElse
}

It occurred to me after this use, Wire could just generate this directly on the Companion object, and without the generics indirection, so it would look like so:

class MyProto(
  // fields
): Message<MyProto, MyProto.Builder>(ADAPTER, unknownFields) {
  // ...
  companion object {
    //...
    fun build(initializer: MyProto.Builder.() -> Unit) : MyProto {
      val builder = MyProto.Builder()
      initializer.invoke(builder)
      return builder.build()
    }
  }
}

I'm working up a PR for this, but I wanted to get some comment prior to proposing it in code.

@oldergod
Copy link
Member

That's an idea we've seen in the past and to be honest I don't think this should be in Wire.

However, it would be possible for one to generate those methods using a custom SchemaHandler. I'm not 100% it'd be easy to get the generated names when conflicts happened but otherwise, the code could live outside Wire, could be customized as much as you want since you'd own it. What do you think?

@cgruber
Copy link
Author

cgruber commented Aug 28, 2023

Why do you not think it should be in wire? We're generating kotlin, why should we not generate kotlin typesafe builders? We already do this outside wire, and it's just simple boilerplate around the java builders anyway. Given the push to avoid constructors, making the builders more kotliny seems entirely within the purvue of kotlin gencode.

@cgruber
Copy link
Author

cgruber commented Aug 28, 2023

I could see this argument if "javaInterop" were implemented via SchemaHandler, because then there's a first-class way to do this, and the very system I was wanting to evolve to include these typesafe builders would also use that. But if javaInterop is "baked in" to Wire, I don't see a great argument why kotlin typesafe builders on top of those shouldn't also be.

@oldergod
Copy link
Member

I've not seen much helper methods within their own class themselves. Everything is built via extension functions in Kotlin stdlib. But I'm not sure it matters much.
PR welcome then. And so this would be generated when either javaInterop or buildersOnly is true, right?

@oldergod
Copy link
Member

Fixed with #2674

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants