-
Notifications
You must be signed in to change notification settings - Fork 356
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add initial Java annotations for Kotlin sugar proposal #111
Closed
JakeWharton
wants to merge
4
commits into
Kotlin:master
from
android:java-annotations-for-kotlin-sugar
Closed
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,393 @@ | ||
# Java annotations for Kotlin sugar | ||
|
||
* **Type**: Design proposal | ||
* **Author**: Jake Wharton | ||
* **Status**: Under consideration | ||
* **Prototype**: Partially implemented in Kotlin 1.1.60 | ||
|
||
|
||
## Feedback | ||
|
||
Discussion of this proposal is held in [this issue](TODO). | ||
|
||
|
||
## Summary | ||
|
||
This proposal adds annotations which enable the use of more Kotlin language | ||
features like extension functions, extension properties, default values, and | ||
property names for Java code. | ||
|
||
* `@ExtensionFunction` / `@ExtensionProperty` - Turn a static method with at | ||
least one argument into an extension function or an extension property. | ||
* `@DefaultValue` - Default parameter values. | ||
* `@KtName` - An alternate name for methods, fields, and parameters for use | ||
by Kotlin code. | ||
|
||
|
||
## Motivation / use cases | ||
|
||
There is a large corpus of Java libraries which, for various reasons, can't | ||
or won't port to Kotlin for the foreseeable future. This fact does not render | ||
them as obsolete or inferior to a Kotlin library. In fact, a certain portion | ||
of Java library authors are sympathetic to Kotlin users and are willing to do | ||
more for compatibility. | ||
|
||
Java libraries following _Effective Java_ item 1 have static factory methods | ||
for adapting arguments into instances of the enclosing type. For example, | ||
to [create a Guava `ByteSource` from a `File`][file-byte-source]: | ||
```kotlin | ||
val file = File("hello.txt") | ||
val source = Files.asByteSource(file) | ||
``` | ||
|
||
This calling convention is significantly different than what would be used | ||
creating an `InputStream` from a `File` with the Kotlin standard library: | ||
```kotlin | ||
val file = File("hello.txt") | ||
val input = file.inputStream() | ||
``` | ||
|
||
A Kotlin-friendly version of Guava's `ByteSource` factory could be changed | ||
to: | ||
```java | ||
@ExtensionFunction | ||
public static ByteSource asByteSource(File file) { .. } | ||
``` | ||
which would allow Kotlin callers to instead use it as an extension: | ||
```kotlin | ||
val file = File("hello.txt") | ||
val source = file.asByteSource() | ||
``` | ||
|
||
To complete the calling convention parity with the standard library, | ||
the function name can be adjusted solely on the Kotlin side: | ||
```java | ||
@ExtensionFunction | ||
@KtName("byteSource") | ||
public static ByteSource asByteSource(File file) { .. } | ||
``` | ||
to enable: | ||
```kotlin | ||
val file = File("hello.txt") | ||
val source = file.byteSource() | ||
``` | ||
|
||
Changing the name from the Kotlin side is essential as the context of the class | ||
name is lost when changing to an extension function. For example, if we naively | ||
translate Guava's `ImmutableSet` factory: | ||
```java | ||
@ExtensionFunction | ||
public static <T> ImmutableSet<T> copyOf(Collection<T> list) { .. } | ||
``` | ||
the function lacks clarity in the operation it performs: | ||
```kotlin | ||
val list = listOf("a", "b", "c") | ||
val immutableSet = list.copyOf() | ||
``` | ||
|
||
With `@KtName` we can again create parity with the calling convention of Kotlin: | ||
```java | ||
@ExtensionFunction | ||
@KtName("toImmutableSet") | ||
public static <T> ImmutableSet<T> copyOf(Collection<T> list) { .. } | ||
``` | ||
```kotlin | ||
val list = listOf("a", "b", "c") | ||
val immutableSet = list.toImmutableSet() | ||
``` | ||
|
||
Static methods annotated with `@ExtensionProperty` will be turned into | ||
extension properties. For example, to [get the root cause of an exception with | ||
Guava][root-cause]: | ||
```kotlin | ||
} catch (e: InvocationTargetException) { | ||
// Original calling convention | ||
val cause = Throwables.getRootCause(e) | ||
} | ||
``` | ||
```java | ||
// Updated Java signature with annotation | ||
@ExtensionProperty | ||
public static Throwable getRootCause(Throwable t) { .. } | ||
``` | ||
```kotlin | ||
} catch (e: InvocationTargetException) { | ||
// New calling convention | ||
val cause = e.rootCause | ||
} | ||
``` | ||
|
||
`@ExtensionProperty` methods with names that match the ["Getters and Setters" | ||
Java-to-Kotlin interop rules][getters] will have the same renaming behavior as | ||
members. Using `@KtName` allows overriding this behavior, if desired. | ||
|
||
While the Java class file format does allow parameter names as of version 52 | ||
(corresponding to Java 8), they are opt-in and almost never included. Once | ||
enabled, they also require committing to stable parameter names for the | ||
entirety of the API surface. `@KtName` can also be used to define stable | ||
parameter names. For example, [Android's `View.setPadding`][android-padding] | ||
has four parameters in an order that can be difficult to remember: | ||
```java | ||
public void setPadding(int left, int top, int right, int bottom) { .. } | ||
``` | ||
By adding `@KtName`, Kotlin callers can specify the parameters in any order: | ||
```java | ||
public void setPadding( | ||
@KtName("left") int left, | ||
@KtName("top") int top, | ||
@KtName("right") int right, | ||
@KtName("bottom") int bottom) { .. } | ||
``` | ||
```kotlin | ||
val view = View(context) | ||
view.setPadding(top = 5, bottom = 5, left = 10, right = 10) | ||
``` | ||
|
||
Default values can also be specified with `@DefaultValue` which are interpreted | ||
as expressions in a similar fashion to [`ReplaceWith`][replace-with]. In | ||
keeping with Android's `View.setPadding` example, we can now add defaults that | ||
look up the current value allowing a subset of values to be passed: | ||
```java | ||
public void setPadding( | ||
@KtName("left") @DefaultValue("paddingLeft") int left, | ||
@KtName("top") @DefaultValue("paddingTop") int top, | ||
@KtName("right") @DefaultValue("paddingRight") int right, | ||
@KtName("bottom") @DefaultValue("paddingBottom") int bottom) { .. } | ||
``` | ||
```kotlin | ||
val view = View(context) | ||
view.setPadding(left = 10, right = 10) | ||
``` | ||
|
||
These annotations can all combine together to adapt a Java API which is | ||
otherwise unidiomatic to something which feels very natural. | ||
```java | ||
@ExtensionFunction | ||
@KtName("toByteString") | ||
public static ByteString of( | ||
byte[] bytes, | ||
@KtName("offset") @DefaultValue("0") int offset, | ||
@KtName("count") @DefaultValue("bytes.size - offset") int count) { .. } | ||
``` | ||
```kotlin | ||
val bytes = byteArrayOf(1, 2, 3) | ||
// without annotations: | ||
val byteString1 = ByteString.of(bytes, 0, bytes.length) | ||
// with annotations: | ||
val byteString2 = bytes.toByteString() | ||
``` | ||
|
||
|
||
## Alternatives | ||
|
||
At present, libraries written in Java that want to be Kotlin friendly can | ||
[add nullness annotations to avoid platform types][platform-types] and can use | ||
well-known names to enable some [operator use][operators] and | ||
[property use][getters]. This is the limit of the Kotlin language features they | ||
have access to. | ||
|
||
A Java library can do one of two things to go farther with its support: | ||
|
||
1. Publish Kotlin extensions inside their library or as a sibling artifact. | ||
2. Rewrite their public API (or the entire library in Kotlin). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only "or the entire library" should be enclosed in the parentheses. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure how this made it in. I thought I fixed it long ago... Thanks! Updated. |
||
|
||
Since rewriting in Kotlin would obviously solve all of the interoperability | ||
problems, option #1 is the only real comparison. [JUnit 5][junit5] and | ||
[Project Reactor][reactor] are two examples which include Kotlin extensions in | ||
their primary artifact but with an optional Kotlin dependency. | ||
[RxKotlin][rxkotlin] and [Android KTX][android-ktx] are two examples of | ||
separate artifacts (for RxJava and the Android framework, respectively) to | ||
provide extensions. | ||
|
||
The majority of features provided by these libraries are adapting Java | ||
static methods into Kotlin instance extensions. Functionality-wise, there | ||
should be no difference between an annotated Java method and a manually-written | ||
Kotlin extension. For the Guava `ByteSource` factory, a zero-overhead extension | ||
can be written: | ||
```kotlin | ||
inline fun File.byteSource(): ByteSource = Files.asByteSource(this) | ||
``` | ||
|
||
Adding parameter names and default values to manually-written Kotlin extensions | ||
works for adapting Java static method but does not fully work for instance | ||
methods. For the Android padding example, you can write an extension that can | ||
enables the use of parameter names for a subset of arguments, but fails when | ||
you try to include all four: | ||
```kotlin | ||
inline fun View.setPadding( | ||
left: Int = paddingLeft, | ||
top: Int = paddingTop, | ||
right: Int = paddingRight, | ||
bottom: Int = paddingBottom | ||
) { | ||
setPadding(left, top, right, bottom) | ||
} | ||
|
||
// works: | ||
view.setPadding(left = 5, right = 5) | ||
// error: | ||
view.setPadding(left = 5, right = 5, top = 10, bottom = 10) | ||
``` | ||
|
||
This is because Kotlin will always prefer calling a real member over an | ||
extension. When all four arguments are provided the member (and its lack of | ||
parameter name support) is resolved. | ||
|
||
As outlined in the "Migration" section, the annotation-based approach enables | ||
Kotlin consumers to be gradually introduced to the Kotlin calling convention | ||
without having to know they're available and without having to seek out a | ||
sibling artifact. | ||
|
||
Other advantages start to become subjective: | ||
|
||
- The annotation-based approach keeps a single source of truth for both Java | ||
and Kotlin callers. This is similar to what `@JvmName` and `@JsName` provide | ||
to Kotlin API authors for controlling how Java and Javascript consumers see | ||
their API. | ||
|
||
A potential downside of this is that you are limited to the functionality | ||
that the annotations provide. | ||
|
||
A single source of truth can be both an advantage and disadvantage for | ||
documentation. For usage samples, it can be challenging to cover the | ||
conventions of each language. The full documentation is always present, | ||
though, regardless of the calling language. Manually-written extensions tend | ||
to contain only a summary and a `@see` directive to the original Java | ||
method. | ||
|
||
- No new compiler / toolchain has to be introduced for the annotation-based | ||
approach. Your compilation and distribution mechanism for Java does not have | ||
to be updated to include Kotlin in the main or a sibling artifact. | ||
|
||
|
||
## Migration | ||
|
||
Since these annotations change the calling convention and/or the name of | ||
functions and properties, their addition is otherwise a source-incompatible | ||
change for Kotlin users. In an effort to mitigate the pain that this would | ||
cause and to guide the consumer to discoverability of the Kotlin-specific | ||
calling convention, both the Java-style and Kotlin-style invocations are | ||
allowed. | ||
|
||
The previous example: | ||
```java | ||
@ExtensionFunction | ||
@KtName("toByteString") | ||
public static ByteString of( | ||
byte[] bytes, | ||
@KtName("offset") @DefaultValue("0") int offset, | ||
@KtName("count") @DefaultValue("bytes.length") int count) { .. } | ||
``` | ||
```kotlin | ||
val bytes = byteArrayOf(1, 2, 3) | ||
// Java-style: | ||
val byteString1 = ByteString.of(bytes, 0, bytes.length) | ||
// Kotlin-style: | ||
val byteString2 = bytes.toByteString() | ||
``` | ||
would compile as-is having both styles in use. | ||
|
||
To gently migrate users to the Kotlin-style syntax, the Kotlin IDEA | ||
plugin would highlight Java-style call sites with a yellow underline. An | ||
intention action would automatically rewrite the Java-style to Kotlin-style, | ||
using default values where appropriate. | ||
|
||
This migration behavior is designed to match what happens when other | ||
Java-style conventions are used such as calling `foo.equals(bar)` (which | ||
should be `foo == bar`) or `foo.getBaz()` (which should be `foo.baz`). | ||
|
||
The compiler would not emit these as actual warnings. | ||
|
||
|
||
## Failure scenarios | ||
|
||
If a library author chooses to ignore validation of any kind and creates an | ||
invalid configuration the Kotlin consumer shouldn't be punished. | ||
|
||
For example, the use of `@KtName` opens up the possibility to create name | ||
collisions: | ||
```java | ||
public static String one() { .. } | ||
|
||
@KtName("one") | ||
public static String uno() { .. } | ||
``` | ||
|
||
In this case the **entire** `uno` method would be rejected from any annotation | ||
enhancement. This is important as the intent can be ambiguous in certain | ||
configurations. For example, both `@ExtensionFunction` and `@ExtensionProperty` | ||
may be present on a single method. | ||
|
||
The Kotlin compiler, when consuming an annotated Java library, should emit a | ||
warning for each Java method with an invalid configuration. The Kotlin IDEA | ||
plugin should not show any warnings inline, not offer any migration assistance, | ||
and treat the method as otherwise being pure, vanilla Java. | ||
|
||
|
||
## Annotation artifact | ||
|
||
Since these annotations are meant for consumption by Java libraries, they | ||
should not be part of the Kotlin standard library. Since they are | ||
Kotlin-specific, they should not be part of `org.jetbrains:annotations`. | ||
|
||
A new artifact, `org.jetbrains.kotlin:kotlin-java-annotations` should be | ||
created to house these annotations. Since the interpreter for these | ||
annotations is the Kotlin compiler and Kotlin IDEA plugin, they should | ||
live in the [JetBrains/kotlin][kotlin-repo] repository and be versioned | ||
and released as part of the normal Kotlin process. | ||
|
||
This artifact could also be the same for the annotations proposed in | ||
[KEEP-99][keep-99]. | ||
|
||
|
||
## Validation | ||
|
||
When adding platform-specific annotations such as `@JvmName` or `@JsName` to | ||
Kotlin code the compiler validates you are not violating the target platform's | ||
language rules. When Java code is being annotated, however, the Kotlin compiler | ||
is not present to perform validation. An annotation processor will be provided | ||
to perform validation. | ||
|
||
The annotation processor artifact will be | ||
`org.jetbrains.kotlin:kotlin-java-annotations-validator` and live in the | ||
[JetBrains/kotlin][kotlin-repo] repository and be versioned and released as | ||
part of the normal Kotlin process. | ||
|
||
In order to get real-time validation in the IDE, the Kotlin IDEA plugin will | ||
also include the validation rules which it will run on Java sources containing | ||
the annotations to show errors. | ||
|
||
|
||
## Scope | ||
|
||
This proposal focuses on only 4 annotations: `@ExtensionFunction`, | ||
`@ExtensionProperty`, `@DefaultValue`, and `@KtName`. These were chosen because | ||
a sampling of Java libraries showed them to have the most impact to Kotlin | ||
consumers if implemented. | ||
|
||
There are certainly other annotations which could be proposed and implemented | ||
to further interop. Ones that come to mind are denoting top-level functions, | ||
lambda parameters becoming lambda-with-receiver, final classes with a static | ||
`getInstance()` method becoming an object, `Class`-accepting generic methods | ||
being automatically provided with reification, and destructing component | ||
markers. While appealing, these are decidedly more rare and would only increase | ||
the complexity of this proposal. If desired they can be pursued at a later | ||
time. | ||
|
||
|
||
|
||
|
||
|
||
[file-byte-source]: https://google.github.io/guava/releases/25.0-jre/api/docs/com/google/common/io/Files.html#asByteSource-java.io.File- | ||
[root-cause]: https://google.github.io/guava/releases/25.0-jre/api/docs/com/google/common/base/Throwables.html#getRootCause-java.lang.Throwable- | ||
[android-padding]: https://developer.android.com/reference/android/view/View#setPadding(int,%20int,%20int,%20int) | ||
[replace-with]: http://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-replace-with/index.html | ||
[kotlin-repo]: https://github.com/JetBrains/kotlin | ||
[keep-99]: https://github.com/Kotlin/KEEP/issues/99 | ||
[getters]: https://kotlinlang.org/docs/reference/java-interop.html#getters-and-setters | ||
[platform-types]: https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types | ||
[junit5]: https://github.com/junit-team/junit5/blob/6b7da8949e8b0f93f7e4f7f2b745ae0988474c9a/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt | ||
[reactor]: https://github.com/reactor/reactor-core/tree/d9d76aba749022466d125890d13e3ba0f23702cd/reactor-core/src/main/kotlin/reactor/core/publisher | ||
[rxkotlin]: https://github.com/ReactiveX/RxKotlin/#readme | ||
[android-ktx]: https://github.com/android/android-ktx#readme | ||
[operators]: http://kotlinlang.org/docs/reference/java-interop.html#operators |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO
should be changed to #110