CLOVIS
01/03/2024, 4:00 PMclass Foo<A, B, C> internal constructor(…)
Users can only instantiate instances via a builder (①):
val foo = Foo.new()
.a(…)
.b(…)
.build()
All type parameters have default values.
Depending on the values of the type parameters, users can pass these instances to different functions that I provide (②):
doSomethingWith(foo) // different overload based on the type parameters
I also provide a type alias that has the common supertype for all type parameters (③):
typealias AnyFoo = Foo<*, *, *>
My understanding is that adding a type parameter:
• is a binary-compatible change, because type parameters are removed at compile-time anyway
• is a source-incompatible change, because the user must specify it
However, since:
• The user doesn't need to write the type parameters when declaring variables, thanks to type inference from the builder output
• The user doesn't need to write the type parameters when instantiating the class (①) nor when using the object (②)
• The user in many cases can use the generic typealias, which is always source- and binary-compatible when type parameters are added (③)
Would you consider it safe to add a type parameter to Foo
in a minor semver release? How can I communicate to users that writing Foo<something>
in their codebase will lead to breaking changes, but using type inference won't?Youssef Shoaib [MOD]
01/03/2024, 7:38 PMFoo
as a parameter or receiver. They'd be forced to specify type parameters then.CLOVIS
01/03/2024, 7:48 PM@Deprecated
for that. I'll need to re-read the @RequiresOptIn
annotation but I believe it applies to all members of the class, not just the class name itself?Daniel Pitts
01/04/2024, 4:29 PMDaniel Pitts
01/04/2024, 4:29 PMDaniel Pitts
01/04/2024, 4:30 PMCLOVIS
01/04/2024, 4:40 PMCLOVIS
01/04/2024, 4:41 PMAlso, I don't think you can overload based on type parameters, so I think you're going to be stuck on that part.You can 🙂
Daniel Pitts
01/04/2024, 4:41 PMCLOVIS
01/04/2024, 4:43 PMIs it possible to manage the types only in the builder, and have the actual type be non-generic?The types carry information that the builder wants to provide to the other functions. For unrelated reasons, these functions need to have that information at compile-time (they are
inline
+ reified
), so I don't have a choice but to use type parameters.Daniel Pitts
01/04/2024, 4:43 PMfun something(foo: Foo<A,B,C>)
and
fun something(foo: Foo<X,Y,Z>)
Will both have the same type erased signature, so the overloading won't work.CLOVIS
01/04/2024, 4:43 PMI feel like you're making your API too clever.Ahah yeah, that might be true.
CLOVIS
01/04/2024, 4:43 PMWill both have the same type erased signature, so the overloading won't work.If you use
@JvmName
etc, you can overload on type parameters even if the platform doesn't allow it.Daniel Pitts
01/04/2024, 4:43 PMCLOVIS
01/04/2024, 4:46 PMinline reified
because of KotlinX.Serialization. Thus, the only way to store the information is in the common object as a type parameter so it can be used as a constraint for both inline reified
functions.CLOVIS
01/04/2024, 4:48 PM// common code
val get = endpointBuilder()
.withBody<Foo>()
// client-side
engine.request(get)
where that last function is implemented as
suspend inline fun <reified B : Body> Engine.request(endpoint: Endpoint<B>) {
// uses KotlinX.Serialization, etc
}
CLOVIS
01/04/2024, 4:49 PMDaniel Pitts
01/04/2024, 4:49 PMKotlinX.Serialization
, so I can't really make suggestions around that specifically.Daniel Pitts
01/04/2024, 4:50 PMCLOVIS
01/04/2024, 4:50 PMreified
, thus all utility methods built upon it must be reified
as well.CLOVIS
01/04/2024, 4:51 PMDaniel Pitts
01/04/2024, 4:51 PMDaniel Pitts
01/04/2024, 4:52 PMCLOVIS
01/04/2024, 4:53 PMDaniel Pitts
01/04/2024, 4:55 PMdata class TypeInfo<T>(val type: KClass<T>)
with a inline fun <reified T> typeOf() = TypeInfo(T::class)
?Daniel Pitts
01/04/2024, 4:56 PMDaniel Pitts
01/04/2024, 4:56 PMCLOVIS
01/04/2024, 4:56 PMTypeInfo
helps?Daniel Pitts
01/04/2024, 4:57 PMDaniel Pitts
01/04/2024, 4:58 PMDaniel Pitts
01/04/2024, 5:02 PMCLOVIS
01/04/2024, 5:04 PMYoussef Shoaib [MOD]
01/04/2024, 5:20 PMCLOVIS
01/04/2024, 5:23 PMCLOVIS
01/04/2024, 5:24 PMCLOVIS
01/04/2024, 5:24 PMYoussef Shoaib [MOD]
01/04/2024, 5:28 PMRequiresOptIn
. Something like Foo<TypeWrapper<A, B, C>>
and typealias AnyFoo = Foo<*>
(so that the alias doesn't need opt in)CLOVIS
01/04/2024, 5:35 PMYoussef Shoaib [MOD]
01/04/2024, 6:07 PMRequiresOptIn
doesn't work, but this does (It's a little bit of an abuse of Deprecated
):
@Deprecated("Don't use", level = DeprecationLevel.HIDDEN)
sealed interface TypeWrapper<A, B, C>
class Endpoint<T>
typealias AnyEndpoint = Endpoint<*>
@Suppress("DEPRECATION_ERROR")
inline fun <reified A, reified B, reified C> Endpoint<TypeWrapper<A, B, C>>.useTypes() {
println("${A::class} ${B::class} ${C::class}")
}
@Suppress("DEPRECATION_ERROR")
fun endpoint(): Endpoint<TypeWrapper<Unit, Unit, Unit>> = Endpoint<TypeWrapper<Unit, Unit, Unit>>()
@Suppress("DEPRECATION_ERROR")
fun <New, A, B, C> Endpoint<TypeWrapper<A, B, C>>.a() = Endpoint<TypeWrapper<New, B, C>>()
@Suppress("DEPRECATION_ERROR")
fun <New, A, B, C> Endpoint<TypeWrapper<A, B, C>>.b() = Endpoint<TypeWrapper<A, New, C>>()
@Suppress("DEPRECATION_ERROR")
fun <New, A, B, C> Endpoint<TypeWrapper<A, B, C>>.c() = Endpoint<TypeWrapper<A, B, New>>()
fun main() {
val myEndpoint = endpoint()
.a<String, _, _, _>()
.b<Int, _, _, _>()
.c<List<*>, _, _, _>()
myEndpoint.useTypes()
}
Youssef Shoaib [MOD]
01/04/2024, 6:11 PMEndpoint<T>
, but it won't allow them to change the type within TypeWrapper
. As long as you define your functions sufficiently, it should be fine.CLOVIS
01/04/2024, 6:11 PMCLOVIS
01/04/2024, 6:12 PMYoussef Shoaib [MOD]
01/04/2024, 6:13 PMrequest
methods by having to use _
to infer types everywhereCLOVIS
01/04/2024, 6:14 PMYoussef Shoaib [MOD]
01/04/2024, 8:55 PMsealed interface AnyEndpoint
@Deprecated("Don't use", level = DeprecationLevel.HIDDEN)
class Endpoint<A, B, C>: AnyEndpoint {
@Suppress("DEPRECATION_ERROR")
fun <A> a() = Endpoint<A, B, C>()
@Suppress("DEPRECATION_ERROR")
fun <B> b() = Endpoint<A, B, C>()
@Suppress("DEPRECATION_ERROR")
fun <C> c() = Endpoint<A, B, C>()
}
@Suppress("DEPRECATION_ERROR")
inline fun <reified A, reified B, reified C> Endpoint<A, B, C>.useTypes() {
println("${A::class} ${B::class} ${C::class}")
}
@Suppress("DEPRECATION_ERROR")
fun endpoint(): Endpoint<Unit, Unit, Unit> = Endpoint<Unit, Unit, Unit>()
fun AnyEndpoint.noSuppressionNeeded() { }
fun main() {
val myEndpoint = endpoint()
.a<String>()
.b<Int>()
.c<List<*>>()
myEndpoint.useTypes()
}
A no-compromise solution. Well, users can't refer to the Endpoint
type, but they shouldn't have anyway. AnyEndpoint
has to be a supertype because `typealias`es also carry the Deprecation it turns out.CLOVIS
01/04/2024, 9:11 PMEndpoint
, does it not?CLOVIS
01/04/2024, 9:14 PMCLOVIS
01/04/2024, 9:15 PMAssuming this indeed works with no downsides, I don't think this is an issue. I think it's intuitive enough that it shouldn't cause issues.has to be a supertypeAnyEndpoint
CLOVIS
01/04/2024, 9:18 PMCLOVIS
01/04/2024, 9:20 PMAny
supertype that isn't deprecated, then the value must come from the author of the deprecated type, who will in the future replace it by another type under the same supertype?