Search code examples
kotlinoperator-overloadingassignment-operatorgradle-kotlin-dsl

How can I define a custom assign operator overload in Kotlin?


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


Solution

  • 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.

    Applying the Kotlin Assignment compiler plugin

    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)

    Setting up assignment overloading

    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).

    Writing assignment operators

    Once the compiler plugin is applied and the annotation is set up, you can start writing overload operators.

    @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
    }
    

    Using assignment operators

    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)
    

    Limitations

    Properties cannot be mutable

    Operator overloading will not work when properties of type StringWrapper are vars. They must be vals.

    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.
    }
    

    Assignment overloading only works for member properties

    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