Extensions and backward compatibility

Kotlin documentation:

If a class has a member function, and an extension function is defined which has the same receiver type, the same name, and is applicable to given arguments, the member always wins.

Suppose you are a library author, and you want to maintain strict backward compatibility of your API.
How do you solve this problem:

// V0
class PublicApi {
	// ...
}

// V1
class PublicApi {
	// add new public member with implementation
	fun foo() {
		// ...
	}
}

// User code using V0
fun PublicApi.foo() {
	// ...
}

User code silently breaks in V1 because of different implementation of extension foo and member foo.

Does this mean that you cannot add new members to the class once you’ve published it?

1 Like

Internally, Kotlin has an Annotation that does exactly that. From the comment on that annotation:

Specifies that a corresponding member has the lowest priority in overload resolution.

It is internal though, but there is a small hack to be able to use it. Please refer to this post on how to exactly do it. It should only take you about 2 minutes lol.

2 Likes

This is not that bad a change than you might think, it will only break code when it is recompiled. Because extension functions are resolved statically this won’t interfere with code that is compiled against an older version of your library.

One solution to the problem would be to only add new member functions in bigger releases (only 1.x insteaad of 1.x.y releases) and then properly document that these functions have been added in the change notes.
Also there is a compiler warning on the extension function, that it shadows an existing member function. Ok, there are projects out there that ignore compiler warnings, but I that’s nothing you can change.

3 Likes

And if you compile a new version, you will get a warning.

Unfortunately, even if that annotation goes public someday, it will not solve the problem. HideMembers increase priority of the marked function (if i correctly understand the docs). That means every time you use 3-rd party API and create extensions for it, you also need to mark every extension with HideMembers, which does not seem practical.

If we are talking about magic annotations, then what we really need is an annotation that does exactly opposite - reduces overload priority, so that i can mark my new API members and don’t worry about user code breaking suddenly. But it seems like a hack anyway.

The only real solution came to my mind is compile-time error on extension/member conflict which will break existing code.

So, ironically, potential backward compatibility issues can not be fully resolved because of backward compatibility.

1 Like

It would only break existing code when it is re-compiled. Already compiled code will continue to use the extension function as stated in a previous post.

A compiler flag would be nice that turns the warnings into errors.

Yes, i know we are talking about source-level compatibility, not binary.

And yes, compiler flag might be a solution if we could force it on in all CI pipelines, but we can’t, and we can’t force people to not ignore warnings.

In my own pipeline, I have prohibited a build with warnings a long time ago. Now people are throwing @Suppress everywhere :grinning:

I do that myself, but I think this is a good thing. A warning means, “hey, this part of your code as a high likelihood of creating errors”. I think of suppress like “Yes, I know and I checked this piece of code, it’s fine here, but please let me know of any other issues you might find”. I mainly use suppress for unchecked casts (when working with generics) but I also tend to use it for function/property/class names not following the coding conventions (eg. for DSLs) or “unused” to add constant extensions to classes.
When to use suppress is something that has to be decided on a case by case basis

There is an open issue for this: https://youtrack.jetbrains.com/issue/KT-18783

1 Like

The link that I referenced is the LowPriorityInOverloadResolution one, which does the exact opposite of HideMembers

LowPriorityInOverloadResolution should be the perfect fit for you as a library author. Just do that little hack that allows you to use it (from the post that I linked to), and then annotate your new foo member function with LowPriorityInOverloadResolution, so then if any user has a foo extension function, it will take over it, but for all the other users who don’t have a foo function, they’ll be able to use your new member function normally. The existence of that annotation suggests that JetBrains probably ran into that same problem and had to solve it that way, which probably means that it should at least work in most of the normal cases.

Example:

// V0
class PublicApi {
	// ...
}

// V1
class PublicApi {
	// add new public member with implementation. Assuming of course that you redefined the annotation as explained in the post
    @LowPriorityInOverloadResolution
	fun foo() {
		// ...
	}
}

// User code using V0
fun PublicApi.foo() {
	// ...
}

// User code that uses the extension that was written with V0, but now is getting compiled against V1
fun main() {
    PublicApi().foo() // resolves to the extension function instead of the member one.
}

// A different user that didn't have an extension foo, but has now updated the library
fun main() {
    PublicApi().foo) // resolves to the new member foo because there aren't any other overloads of the foo function
}
1 Like