13.7.2019 An Object Spread Operator for Kotlin

Kotlin supports an array spread operator , similar to ​ in modern JavaScript versions like ES6 or the splat operator in Python, but it does not have an object spread operator like … ES6 or ** in Python 3.5.

This articles demonstrates how at least some uses cases for an object spread operator can be implemented with Kotlin.

If you are generally familiar with the spread operator, you can directly jump to An Object Spread Operator for Kotlin below.

The Object Spread Operator in ES6

With the ES6 object spread operator it’s possible to combine the properties of multiple objects as well as single properties to a new object as shown in this example:

// JavaScript needs property names
const fullName = { first: 'Henry', last: 'O\'Conner' }; 

const streetAddress = 
        { street: 'Dog Basket', city: 'Canine Town', zipCode: 'K9T' };

const fullAddress = { …fullName, …streetAddress, planet: 'Earth' };

The third line takes all properties from fullName and then all properties from streetAddress and finally adds the property for 'planet': 'Earth' to create a new object fullAddress. If streetAddress contained a property name which is also in fullName, the property from streetAddress would win as it comes further right in the parameter list.

There is also a similar syntax to combine arrays in ES6.

The Array Spread Operator in Kotlin

The Kotlin * operator is very similar to the ES6 array spread operator shortly mentioned above. In Kotlin it can be used either for spreading an array onto single vararg parameters:

 // given some method taking variable arguments
 fun someMethod(vararg values: Int) {
     // whatever it needs to do with the values
 }

 // and an array
 val arr = arrayOf(1, 2, 3)

 // then the array elements can be passed
 foo(*arr)

This is similar to Java, where an array can be directly passed as varargs.

But the Kotlin syntax is more flexible than in Java and thus more like in ES6. It’s possible to spread multiple arrays and even add single values as shown here:

 val arr1 = arrayOf("value 1", "value 2")
 val arr2 = arrayOf("value 4", "value 6", "value 7")
 someVarargsFunction("value 0", *arr1, "value 3", *arr2)

An Object Spread Operator for Kotlin

I found two options for implementing an object spread operator with Kotlin:

  • Using a Map as the actual property-store of the object and the by operator to create getters for each property.
    This approach cannot be applied to data classes because it needs a primary constructor with a Map.
  • Using extension functions to convert objects from and to Map instances.
    This approach combines very well with data classes, but does not require those.

In both cases, combining properties from multiple objects is done by combining the maps.

1st Approach:
Using a Map as the Actual Property-Store

The first approach to implement an object spread operator with Kotlin is based on classes which store their properties in a Map. Therefore, it needs access to the source code of the classes to which we want to apply it.

class FullNameMC(val map: Map) {
     var first: String by map
     var last: String by map

     // optional if you want simple direct construction from values
     // but it makes the map-based-class more verbose
     constructor(first: String, last: String) :
             this(mutableMapOf("first" to first, "last" to last)) {
     }
 }

Unfortunately this approach cannot be combined with data classes.

But because the map is the actual property store, it can be directly used:

 // create one example object from a map
 val map = mutableMapOf("first" to "Henry", "last" to "O'Conner")
 val fullName = FullNameMC(map)
 
 // create another sample object by constructor
 val streetAddress = StreetAddressMC("Dog Basket", "Canine Town", "K9T")
 
 // combine the two with an additional property to a new object
 val fullAddress = FullAddressMC(
          fullName() + streetAddress() + ("planet" to "Earth"))

The Magic behind the () and + Operators

The last line of code might look like magic. If you have a closer look, it uses the Kotlin invoke operator (), which kinda replaces the ES6​ operator. And this () operator is overwritten to implement the magic:

operator fun Any.invoke(): Map {
     return javaClass.kotlin.memberProperties
             .filter { it.visibility == KVisibility.PUBLIC }
             .map { it.name to it.getter.call(this) }
             .toMap()
 }

Finally, the Kotlin + operator can be applied to the resulting maps as well as the additional property.

2nd Approach:
Converting Objects from and to Map Instances

But we would like an object spread operator which can be applied to any existing class, right? That’s possible with this second approach.

 // create one example object from a map
 val map = mutableMapOf("first" to "Henry", "last" to "O'Conner")
 val fullName = FullNameDC::class.by(map)
 
 // create another sample object by constructor
 val streetAddress = StreetAddressDC(
         "Dog Basket", "Canine Town", "K9T")
 
 // combine the two with an additional property to a new object
 val fullAddress = FullAddressDC::class.by(
         fullName() + streetAddress() + ("planet" to "Earth"))

The Magic of the by Function

ℹ️ See also The Magic behind the () an + Operators in the previous section.

We already know that the actual argument of the by function in the above example is a Map. As there is no by function in Kotlin classes, it’s of course an extension function.

Kotlin extension functions can be used wherever we import these. The following concrete approach needs all properties to be settable in the constructor. But many other implementation could be imagined, e.g. calling setters of the target class. Thus it should be possible to use this general approach for all existing classes, as long as these represent something like properties.

Here one example implementation of such a by extension function:

fun  KClass.by(values: Map): T {
     val ctor = this.primaryConstructor!!
     val params = ctor.parameters
             .associateBy({ it }, { values.get(it.name) })
     return ctor.callBy(params)
 }

Where a normal function could also do the job, an extension function makes the code read more natural.

The magic relies in the associateBy call:

  • It iterates over all declared parameters of the constructor of the target class.
  • For each declared parameter it associates it with the value of the respective value from the values-Map.
  • The result is a Map<KParameter, Any?> which can be used to call the constructor.

⚠️ This implementation does simply ignore map elements which are not also properties of the underlying class. Though, a production-ready implementation of this extension function could implement a check and throw an exception.

Final Thoughts

The approach with the extension function for Kotlin classes using associateBy is more flexible because it can be applied to almost all existing classes which somehow represent properties. It’s only drawback is the little lengthy constructor call TargetClass::class.by(map); maybe one of my readers finds a way to write it as TargetClass.by(map) or similar, which does not need access to the target class source code.

💡 Did you know that in Kotlin you can have default argument values and named arguments? E.g. FullAddressDC(city = "Canine Town", zipCode = "K9T", planet = "Earth") These two features make object creation look similar to JavaScript and combine very well when dealing with properties in general.

This example also showed that Kotlin can do awesome things and Kotlin code can be almost as terse as JavaScript code. Try this with Java!

The source code can be found in my github repository.
You can also reply on Twitter.


References on kotlinlang.org: