Kotlin
A concise multiplatform language developed by JetBrains
Call for Feedback: Upcoming Changes in Kotlin
As mentioned before, we are wrapping up with the language design, and this post is a head-up for the upcoming changes + request for your feedback.
Backing fields
I mentioned some time ago that we are not happy with the present syntax of backing fields, which is $propertyName
:
var foo: Foo = ... get() { beforeRead(); return $foo } set(v) { beforeWrite($foo, v); $foo = v }
The biggest issue is that this syntax clashes with the syntax of string templates.
So, we decided to change the rules here:
- the
$foo
syntax will be deprecated and then dropped - instead, we can access the backing field by the name
field
inside getters/setters:
var foo: Foo = ... get() { beforeRead(); return field } set(v) { beforeWrite(field, v); field = v }
Note that field
is just an implicitly defined variable (very much like it
in lambdas).
Some use cases are not supported by this approach: we used to be able to access backing fields anywhere in the class, and now it’s only visible in getters/setters. We have examined Kotlin code on GitHub, and realized that only a tiny fraction of use cases were not covered, and for these we can always resort to “backing property”:
private var _foo = ... public var foo: Foo get() = ... set(v) { ... }
Operators and infix functions
This has been debated a lot in the past, and we finally decided that we want to introduce some more discipline into how operator overloading and infix function calls work in Kotlin. At the moment any function named plus
that can be called as a.plus(b)
can also be called as a + b
. We will require such functions to be marked with the operator
modifier, otherwise the operator notation will not be available for them. This makes operator use more disciplined and eliminates the possibility of random punctuation creeping into APIs. The most common example would be having a method called get
but totally not intending it for use as square brackets.
Same for infix function calls: we will require a function to be marked as infix
. This will reduce the unintended diversity of styles in common APIs:
list add 1
vslist.add(1)
list map {...}
vslist.map {...}
- etc
Infix functions will be still callable with the old standard syntax x.or(y)
, but the tooling will be hinting to you that the intended syntax is infix.
Note that common functions in the standard library (e.g. map
or filter
) will not be marked as infix
, because using them as such sometimes causes cryptic errors if such an expression is followed by a dot:
list map {...}.toSet() // Error: toSet() is not applicable to a lambda
If some Java method is not marked as operator
or infix
, we can always define an extension that is, and the standard library will provide such extensions for most popular cases.
Constants
Compile-time constants are important when it comes to annotations: only they can be used as arguments (along with very few extra expressions, namely arrays and annotation constructors). So far we took the same “implicit” approach to detecting them as Java does: if a val
in an object
or on the top level only has only constants in its initializer, it is a compile-time constant. This is fragile and presents a possibility of breaking APIs without knowing, so we decided to require the const
modifiers on such val
s:
const val SCREEN_WIDTH = 2048
Note: const
values can only have the following types: “primitives”, String
, enums, class literals.
invokeExtension() convention
This has been pretty obscure so far, but we are going to change it anyways. For now if a value needs to be callable as an extension function, it has to have a member that is an extension and is named invoke
:
class Foo { operator fun String.invoke() { ... } } fun test(foo: Foo) { "".foo() }
This is inconvenient in some cases, so we are going to change this to
class Foo { operator fun invokeExtension(s: String) { ... } } fun test(foo: Foo) { "".foo() }
As a side-effect, it will be possible to add such function as an extension:
class Foo operator fun Foo.invokeExtension(s: String) { ... }
Internal visibility and mangling
Internal members are compiled to public
at the moment, which may lead to accidental overrides:
// module X open class Base { internal fun foo() {...} } // module Y class Derived : Base() { fun foo() {...} }
The compiler will not require override
on Derived::foo
because the parent function is not visible, but in the byte code these have the same signature, and the runtime will bind them as overrides, which was not intended by the authors. The problem is most painful when modules X and Y evolve independently (e.g. one is a library and the other — user’s project), so that when Y is compiled foo
was not yet present in X
.
To avoid this, we decided to mangle names of internal members so that they do not clash with superclass members.
Update: mangling will likely cause this members to be impossible to call from Java. This seems to be hard to fix, but the workaround is straightforward: just make it public
or protected
.
Other changes
- Default implementation classes for interfaces on Java 6 will be named
Foo.DefaultImpls
instead ofFoo$$TImpl
_
,__
,___
will be forbidden as an identifiers, i.e. we can use_foo
, but not_
alone (reserved for future use)- We are going to drop
final
,protected
andinternal
in interfaces: these can not be expressed on the JVM, so we postpone their implementation until later - We are going to drop
identityEquals()
function in favor of===
Feedback
Your opinions and use cases are most welcome!