From c0dc7f15d9cc91f9ef773f881ac67c8cc7470528 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Thu, 10 May 2018 09:53:57 -0700 Subject: [PATCH 1/4] Add initial Java annotations for Kotlin sugar proposal --- .../java-annotations-for-kotlin-sugar.md | 393 ++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 proposals/java-annotations-for-kotlin-sugar.md diff --git a/proposals/java-annotations-for-kotlin-sugar.md b/proposals/java-annotations-for-kotlin-sugar.md new file mode 100644 index 000000000..f05c729b3 --- /dev/null +++ b/proposals/java-annotations-for-kotlin-sugar.md @@ -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 ImmutableSet copyOf(Collection 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 ImmutableSet copyOf(Collection 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). + +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 \ No newline at end of file From 22c110e4c25ff53ae6f4d12d01e8863e734e4f96 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Fri, 11 May 2018 19:22:13 -0700 Subject: [PATCH 2/4] Add issue link. --- proposals/java-annotations-for-kotlin-sugar.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/proposals/java-annotations-for-kotlin-sugar.md b/proposals/java-annotations-for-kotlin-sugar.md index f05c729b3..9c4bd5f74 100644 --- a/proposals/java-annotations-for-kotlin-sugar.md +++ b/proposals/java-annotations-for-kotlin-sugar.md @@ -8,7 +8,8 @@ ## Feedback -Discussion of this proposal is held in [this issue](TODO). +Discussion of this proposal is held in +[this issue](https://github.com/Kotlin/KEEP/issues/110). ## Summary From c75f7b00a84b45c5a06683375e9b4c4d01d0e849 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Tue, 15 May 2018 14:20:27 -0400 Subject: [PATCH 3/4] Move 'in Kotlin' outside parenthesis. --- proposals/java-annotations-for-kotlin-sugar.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/java-annotations-for-kotlin-sugar.md b/proposals/java-annotations-for-kotlin-sugar.md index 9c4bd5f74..48986959e 100644 --- a/proposals/java-annotations-for-kotlin-sugar.md +++ b/proposals/java-annotations-for-kotlin-sugar.md @@ -190,7 +190,7 @@ 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). + 2. Rewrite their public API (or the entire library) in Kotlin. Since rewriting in Kotlin would obviously solve all of the interoperability problems, option #1 is the only real comparison. [JUnit 5][junit5] and From b98901b4b5c07e4ea4721b42b612ac769de08c4f Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Tue, 15 May 2018 15:24:25 -0400 Subject: [PATCH 4/4] Update based on 2018-05-10 discussions. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Two extension annotations or one? * @Extension(type=FUNCTION/PROPERTY) * Okay, except when you add the name into this annotation the declaration site becomes very verbose. * @Extension + inferred from name * Name doesn’t always match intent to be a property * @ExtensionFunction or @ExtensionProperty * Two annotations * Bean prefix removal only applies to one * Extension on method or on first parameter (receiver) * Method * Parameter * Like C# * Can be used on parameters which aren’t the first * Name inside parameter or separate annotation * @ExtensionFunction(“name”) * @ExtensionFunction @Name(“name”) * @Name can be re-used for parameter names * Lets @Name be used places it isn’t desired * Lets @Name be used without @ExtensionFunction which we don’t like Outcome: * When the name is combined with the annotation it makes more sense to have two annotations so you don’t have to supply name. * Two default value annotations or one with null literal * @DefaultValue(“null”) * Ambiguous between “null” string or null reference * @DefaultNull or @DefaultValue(“0”) * Explicit, no edge cases * Default for collections? * What about collection types we don’t know about? * Expressions in a default? * No time to discuss * @ParameterName * No time to discuss Outcome: * No changes. Need to revisit. 1. Rename @KtName to @ParameterName, scope only to parameters 2. Add name property to @ExtensionFunction and @ExtensionProperty --- .../java-annotations-for-kotlin-sugar.md | 69 +++++++++---------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/proposals/java-annotations-for-kotlin-sugar.md b/proposals/java-annotations-for-kotlin-sugar.md index 48986959e..82eb28328 100644 --- a/proposals/java-annotations-for-kotlin-sugar.md +++ b/proposals/java-annotations-for-kotlin-sugar.md @@ -20,9 +20,8 @@ property names for Java code. * `@ExtensionFunction` / `@ExtensionProperty` - Turn a static method with at least one argument into an extension function or an extension property. + * `@ParameterName` - An explicit name for parameters. * `@DefaultValue` - Default parameter values. - * `@KtName` - An alternate name for methods, fields, and parameters for use - by Kotlin code. ## Motivation / use cases @@ -63,8 +62,7 @@ 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") +@ExtensionFunction("byteSource") public static ByteSource asByteSource(File file) { .. } ``` to enable: @@ -86,10 +84,10 @@ val list = listOf("a", "b", "c") val immutableSet = list.copyOf() ``` -With `@KtName` we can again create parity with the calling convention of Kotlin: +By supplying a name we can again create parity with the calling convention of +Kotlin: ```java -@ExtensionFunction -@KtName("toImmutableSet") +@ExtensionFunction("toImmutableSet") public static ImmutableSet copyOf(Collection list) { .. } ``` ```kotlin @@ -120,24 +118,26 @@ public static Throwable getRootCause(Throwable t) { .. } `@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. +members. Supplying an alternate name 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 +entirety of the API surface. `@ParameterName` can 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: +By adding `@ParameterName`, 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) { .. } + @ParameterName("left") int left, + @ParameterName("top") int top, + @ParameterName("right") int right, + @ParameterName("bottom") int bottom) { .. } ``` ```kotlin val view = View(context) @@ -150,10 +150,10 @@ 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) { .. } + @ParameterName("left") @DefaultValue("paddingLeft") int left, + @ParameterName("top") @DefaultValue("paddingTop") int top, + @ParameterName("right") @DefaultValue("paddingRight") int right, + @ParameterName("bottom") @DefaultValue("paddingBottom") int bottom) { .. } ``` ```kotlin val view = View(context) @@ -163,12 +163,11 @@ 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") +@ExtensionFunction("toByteString") public static ByteString of( byte[] bytes, - @KtName("offset") @DefaultValue("0") int offset, - @KtName("count") @DefaultValue("bytes.size - offset") int count) { .. } + @ParameterName("offset") @DefaultValue("0") int offset, + @ParameterName("count") @DefaultValue("bytes.size - offset") int count) { .. } ``` ```kotlin val bytes = byteArrayOf(1, 2, 3) @@ -272,12 +271,11 @@ allowed. The previous example: ```java -@ExtensionFunction -@KtName("toByteString") +@ExtensionFunction("toByteString") public static ByteString of( byte[] bytes, - @KtName("offset") @DefaultValue("0") int offset, - @KtName("count") @DefaultValue("bytes.length") int count) { .. } + @ParameterName("offset") @DefaultValue("0") int offset, + @ParameterName("count") @DefaultValue("bytes.length") int count) { .. } ``` ```kotlin val bytes = byteArrayOf(1, 2, 3) @@ -305,16 +303,15 @@ The compiler would not emit these as actual warnings. 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: +For example, the ability to supply an alternate name for extensions and +parameters opens up the possibility to create name collisions: ```java -public static String one() { .. } - -@KtName("one") -public static String uno() { .. } +public static String one( + @ParameterName("one") int one, + @ParameterName("one") int uno) { .. } ``` -In this case the **entire** `uno` method would be rejected from any annotation +In this case the **entire** `one` 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. @@ -362,9 +359,9 @@ 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. +`@ExtensionProperty`, `@DefaultValue`, and `@ParameterName`. 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,