Kotlin enables the extension of existing types. For example we can do this:
fun String.replaceSpaces(): String {
return this.replace(' ', '_')
}
val formatted = str.replaceSpaces()
However in JavaScript this is an antipattern.
Does Kotlin sidestep the issues that this causes in Javascript?
Kotlin enables the extension of existing types. For example we can do this:
fun String.replaceSpaces(): String {
return this.replace(' ', '_')
}
val formatted = str.replaceSpaces()
However in JavaScript this is an antipattern.
Does Kotlin sidestep the issues that this causes in Javascript?
Share Improve this question edited Apr 2, 2023 at 19:35 Donald Duck 8,89223 gold badges79 silver badges102 bronze badges asked Oct 19, 2017 at 5:03 OleOle 47k68 gold badges237 silver badges441 bronze badges6 Answers
Reset to default 9No this is not an antipattern. In js its an antipattern because js is dynamic and therefore changing a prototype changes how code works at runtime making it an antipattern. This is also extremely dangerous based on how the in operator works and based on the fact that you can rewrite everything, so changing a prototype can affect code somewhere on your page:
Number.prototype.toString = function(){
return "bullshit";
};
alert(""+12);
In kotlin this is not the case as kotlin is static, and all references are built at compile time. Additionally, you cant overwrite existing methods so its not dangerous at all.
You cannot compare a prototyped language like JS with Kotlin. All extensions are resolved statically and do not modify the extended type ("receiver"). This is really important and invalidates your worry. Please have a look at the documentation to learn more about the things happening with extensions in the background (compiler).
In my opinion, you need to be cautious with extensions though. Don't allow each and every developer in a Kotlin project to add new extensions to random types. I think the project must define certain rules handling the process of defining new functions/properties on existing types, because otherwise it can get hard to read foreign code. Also there should be firm arranged locations where to put those extensions.
The main arguments against extending prototypes in JavaScript are twofold:
- The methods you add might have the same name as methods added by some library used in the same application, but have different behaviour.
- Future version of JavaScript/ECMAScript itself might include a method with the same name as the one you're adding, but with different behaviour.
In JavaScript, both of these scenarios will lead to the wrong version of a method being called at runtime, unexpectedly, resulting in a runtime crash or unexpected runtime behaviour.
In Kotlin, most scenarios akin to this will result in a compile-time error or at least a warning, and so the perils encountered when extending types in JavaScript are mostly avoided in Kotlin. However, there is still some slight scope to encounter similar runtime bugs in rare cases.
To illustrate this, let's write some examples of code where conflicting implementations of a method exist, and see what happens.
Scenario 1: Two libraries provide an extension method with the same signature
Suppose we have this code, in Main.kt
and Library1.kt
respectively:
import library1.*
import library2.*
fun main() {
listOf(99, 101, 103, 104).printOddVals()
}
package library1
fun List<Int>.printOddVals() {
for (x in this) {
if (x % 2 != 0) {
println(x)
}
}
}
So the library1
package defines a printOddVals
method that prints the odd values in a list, and main()
uses it. Now suppose that in library2
, we introduce a conflicting printOddVals
method, that prints the values with odd indices:
package library2
fun List<Int>.printOddVals() {
for ((i, x) in this.withIndex()) {
if (i % 2 != 0) {
println(x)
}
}
}
In the equivalent scenario in JavaScript, this would probably cause a runtime bug. In Kotlin, it merely leads to a compile-time error:
Main.kt:5:31: error: overload resolution ambiguity:
public fun List<Int>.printOddVals(): Unit defined in library1 in file Library1.kt
public fun List<Int>.printOddVals(): Unit defined in library2 in file Library2.kt
listOf(99, 101, 103, 104).printOddVals()
^
IntelliJ IDEA will also tell you how to fix the issue - by introducing an import alias:
Do that, and we get this code, with the ambiguity about which printOddVals
we want to call resolved:
import library1.*
import library2.*
import library2.printOddVals as printOddVals1
fun main() {
listOf(99, 101, 103, 104).printOddVals1()
}
Scenario 2: A library introduces a member function that shadows an extension method you already wrote
Suppose we have the following files:
package library1
class Cow {
fun chewCud() {}
}
import library1.*
fun Cow.moo() {
println("MOOOOOOO!")
}
fun main() {
val cow = Cow()
cow.moo()
}
So Cow.moo
is initially an extension method we wrote. But then we update library1, and then new version has a moo
member function:
package library1
class Cow {
fun chewCud() {}
fun moo() { println("moo") }
}
Now, because member functions are preferred to extension functions when resolving method calls, our extension function is not used when we call cow.moo()
in main()
, and then library's new member function is used instead. This is a change in runtime behaviour, and potentially a bug if the library's new moo()
implementation isn't an adequate substitute for the extension function we'd written before. However, the saving grace is that this at least produces a compiler warning:
Main.kt:3:9: warning: extension is shadowed by a member: public final fun moo(): Unit
fun Cow.moo() {
^
Scenario 3: A library introduces a member function that shadows an extension method you already wrote, and doesn't produce a compiler warning
Once we add inheritance (or interface implementation) into the mix, we can contrive a situation similar to the one above where an extension function we were previously using gets partially shadowed by a member function after a library update, causing changes in runtime behaviour, and no compiler warning occurs.
This time, suppose we have these files:
import library1.*
fun Animal.talk() {
println("Hello there! I am a " + this::class.simpleName)
}
fun main() {
val cow = Cow()
val sheep = Sheep()
cow.talk()
sheep.talk()
}
package library1
interface Animal
class Cow : Animal
class Sheep : Animal
At first, when we run our main()
function, our extension function gets used for both cow.talk()
and sheep.talk()
:
Why hello there. I am a Cow.
Why hello there. I am a Sheep.
. But suppose we add a talk
member to Cow
:
package library1
interface Animal
class Cow : Animal {
fun talk() {
println("moo")
}
}
class Sheep : Animal
Now if we run our program, the member function on Cow
gets preferred to the extension method, and our program's behaviour has been changed - all with no compiler errors or warnings:
moo
Why hello there. I am a Sheep.
So although extension functions are mostly safe in Kotlin, there's still a tiny bit of potential to run into the same pitfalls as in JavaScript.
Is this theoretical danger sufficient to mean that extensions should be used sparingly, or perhaps avoided entirely? Not according to the official Coding Conventions, which take the view that extensions are great and you should use them lots:
Extension functions
Use extension functions liberally. Every time you have a function that works primarily on an object, consider making it an extension function accepting that object as a receiver. To minimize API pollution, restrict the visibility of extension functions as much as it makes sense. As necessary, use local extension functions, member extension functions, or top-level extension functions with private visibility.
You are, of course, free to form your own view!
No it is not good. It is very bad that Kotlin allows extensions of existing types.
With extensions there is no reason at all to create a class hierarchy or even create a new class definition for that matter.
Kotlin creators might as well have just used erlang instead of going to the trouble of creating extensions.
Extensions mean that you can no longer rely on a class definition to be constant. Imagine all the time people are going to spend just finding a developer's "creative" extensions let alone debugging them.
Several languages had this already. The problem with JavaScript is the way it works, as stated in the answer of the link.
JavaScript has a couple way of overriding property. A library could have very well defined a override, but the other one override it again. The function get called from both library, all hell breaks loose.
It’s more a type system and visibility issue in my opinion.
You should have compiled this example and seen the generated code. Everything would become clear:
function replaceSpaces($receiver) {
return replace($receiver, 32, 95);
}
function foo(str) {
var formatted = replaceSpaces(str);
}
There's no monkey patching at all! Extension functions are just a syntactic sugar in Kotlin. It's just another way of passing first argument to a static function.