Avoid Kotlin 'Platform declaration clash' with functions

If 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.