I have a Kotlin class that wraps a mutable value.
class StringWrapper(
var value: String = ""
) {
override fun toString(): String = value
}
I use this wrapper as properties in a custom data holder class
class DataHolder {
val name = StringWrapper()
override fun toString(): String = "Data(name=$name)"
}
I want to make it easier to assign values to the contents of StringWrapper
val dataAlpha = DataHolder()
// the '.value' is unnecessary noise
dataAlpha.name.value = "alpha"
// I want to directly assign a string value, but I get an error
dataAlpha.name = "alpha" // ERROR Type mismatch.
// Required: StringWrapper
// Found: String
I also want to make it easier to copy one StringWrapper
to another.
val dataAlpha = DataHolder()
dataAlpha.name = "alpha"
val dataAlphaCopy = DataHolder()
// I want to directly assign the value of `dataAlpha.name.value` into `dataAlphaCopy.name.value`
dataAlphaCopy.name = dataAlpha.name // ERROR Val cannot be reassigned
I understand that Gradle 8.1 has a new experimental feature in the Kotlin DSL that does what I want. How can I introduce the same assignment operators in my own library?
I have tried looking in the operator overloading docs, but there's no reference to an assignment operator.
There is a KEEP language proposal for introducing such a feature, but it was closed.
I'm using Kotlin 1.8.20
There's a new Kotlin (v1.8.0) compiler plugin that can be used to provide operator loading.
It hasn't been announced yet, but it's available for use. It's in the Kotlin source code here. It's the same plugin that Gradle is using in the Kotlin DSL in Gradle version 8.1.
Support in IntelliJ might be limited - make sure you're on the latest versions.
The Kotlin Assignment plugin can be applied like other Kotlin compiler plugins.
In Gradle projects it can be applied using a simple Gradle plugin.
I'm not familiar with compiling Kotlin using Maven, Ant, or via the CLI - but look at the other Kotlin compiler plugin instructions for similar instructions.
ephemient has shared CLI instructions on Slack
$KOTLIN_HOME/bin/kotlinc-jvm -Xplugin=$KOTLIN_HOME/lib/assignment-compiler-plugin.jar -P plugin:org.jetbrains.kotlin.assignment:annotation=fqdn.SupportsKotlinAssignmentOverloading ...
(you can see all the strings in https://github.com/JetBrains/kotlin/blob/master/plugins/assign-plugin/assign-plugin.common/src/org/jetbrains/kotlin/assignment/plugin/AssignmentPluginNames.kt)
First create an annotation in your project
package my.project
/** Denotes types that will be processed by the Kotlin Assignment plugin */
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class KotlinAssignmentOverloadTarget
and then apply the plugin in Gradle, and configure it to use your annotation
plugins {
kotlin("plugin.assignment") version "1.8.10"
}
assignment {
annotation("my.project.KotlinAssignmentOverloadTarget")
}
Then in your code apply the annotation to StringWrapper
.
@KotlinAssignmentOverloadTarget
class StringWrapper(
var value: String = ""
) {
override fun toString(): String = value
}
I recommend that the annotation should typically be applied to a 'wrapper' class that contains one (or possibly more) mutable values. It could also be applied to an interface which other similar classes implement (which is what Gradle does).
Once the compiler plugin is applied and the annotation is set up, you can start writing overload operators.
assign
and return Unit
.operator
modifier typically used for operator overloading.@KotlinAssignmentOverloadTarget
class StringWrapper(
var value: String = ""
) {
// member function
/** Provides overloaded setter for setting the value of [value] using an assignment syntax */
fun assign(value: String) {
this.value = value
}
}
// extension function
/** Provides overloaded setter for setting the value of [value] using an assignment syntax */
fun StringWrapper.assign(value: StringWrapper) {
this.value = value.value
}
You can now directly assign strings to the name
property
val dataAlpha = DataHolder()
dataAlpha.name = "alpha"
println(dataAlpha) // prints: Data(name=alpha)
And also, using the extension function, assign one StringWrapper
to another.
val dataAlphaCopy = DataHolder()
dataAlphaCopy.name = dataAlpha.name
println(dataAlphaCopy) // prints: Data(name=alpha)
Operator overloading will not work when properties of type StringWrapper
are var
s. They must be val
s.
class MutableDataHolder {
var name = StringWrapper()
}
fun main() {
val dataAlpha = MutableDataHolder()
// no overload operator is generated, because name is a 'var'
dataAlpha.name = "alpha" // ERROR Type mismatch.
}
Keep in mind that assignment overload only works when the properties are member properties.
So, when there's a StringWrapper
value that is not a class property, the assignment overloader won't work.
val nameValue = StringWrapper()
nameValue = "123" // ERROR Val cannot be reassigned, and Type mismatch
Either manually call the assign()
function, or create a class with a property. Using an object expression also works.
val nameValue = StringWrapper()
// manually call the 'assign' function
nameValue.assign("123")
val values = object {
val name = StringWrapper()
}
values.name = "123" // 'name' is a member property, so the assignment overload works
In Kotlin classes the values of properties can be assigned immediately or later, in an init {}
block.
If the property also supports overload assignment then this can completely prevents the property from being initialised.
@KotlinAssignmentOverloadTarget
class StringWrapper(
var value: String = ""
) {
fun assign(value: String) {
this.value = value
}
fun assign(value: StringWrapper) {
this.value = value.value
}
}
class SomeOtherClass {
val stringWrapper: StringWrapper
init {
// RUNTIME ERROR - this uses the assignment operator,
// preventing the property from being initialised.
stringWrapper = StringWrapper()
}
}
There's no helpful compilation warning, and it will fail with an unfriendly runtime error.
Cannot invoke "StringWrapper.assign(StringWrapper)" because "this.stringWrapper" is null
It's possible to work around this in simple situations by immediately assigning the property, but in more complex situations it could be very difficult.
class SomeOtherClass {
// Because StringWrapper supports assignment overloading
// the property must be instantiated immediately.
val stringWrapper: StringWrapper = StringWrapper()
}