Avoid Kotlin 'Platform declaration clash' with functions
22 Jan 2023If you are used to using method overloading in Kotlin you’ll know you can declare the following functions just fine:
fun build(body: Scope) {}
fun build(body: ExtendedScope) {}
What is slightly surprising on first glance is that the following two functions will not compile:
fun build(body: Scope.() -> Unit) {}
fun build(body: ExtendedScope.() -> Unit) {}
The above will output the following warning
Platform declaration clash: The following declarations have the same JVM signature (build(Lkotlin/jvm/functions/Function1;)V):
The warning contains the clue that both functions are actually represented with the synthetic Function1
class like so:
Function1<Scope, Unit>
Function1<ExtendedScope, Unit>
When the bytecode is generated the generics are erased so the compiler just sees two functions with the same type of Function1
.
To get this compiling we need two distinct signatures but it would be nice to keep the ergonomics or being able to invoke the function with a trailing closure.
One way to achieve this is by giving our functions a named type - for this functional interfaces work nicely. In the below I’ve added a couple of function interfaces and updated the original functions
fun interface ScopeFunction {
operator fun invoke(scope: Scope)
}
fun interface ExtendedScopeFunction {
operator fun invoke(scope: Scope)
}
fun build(body: ScopeFunction) {}
fun build(body: ExtendedScopeFunction) {}
With this change the JVM can now compile but we’ve lost some ergonomics in how the function is used.
With the original Scope.() -> Unit
the receiver inside a trailing closure would be Scope
build { // this: Scope
}
With the new change there is no receiver and instead we are provided Scope
as a parameter
build { // it: Scope
}
Consumers of this api can work around this by accepting that that they need to prefix calls to Scope
functions with it
or by with(it) { ... }
to change the receiver.