Kotlin even allows the plusAssign operator “+=” to be overloaded. So, why not the assignment operator?
It can lead to very misleading code. Operators like plusAssign
or minusAssign
are overloadable for optimization reasons.
E.g. why concatenate two lists and then assign instead of just adding an element to a lit? list += "Hello!"
Additionally, it would not fare well with Java support. How would it even work?
you can technically overload it for a property using a delegate
No you can’t. That whould require the delegate to be part of the function signature. The compiler can’t actually know if a super class implements a property with a delegate or not.
@vngantk What’s your use-case?
I’m not familiar with use-cases outside of the C++ self assign example that seems to be common in explaining assignment overriding.
I meant that depending on the usecase you can use a setter as kind of overriding the assignment operator. You can also tell the users of your library that in order to get correct behaviour, they need to use your delegate for any property of that type.
Basically at a certain level the =
operator gets “overridden” but only using a setter or a delegate, and so you need the consumer of your type to enforce that.
A valid use case is creating cleaner DSL-s.
For example, I have a Json DSL, but now I use += for assignment:
val json = jsonObject {
"field" += 1
}
It would be much cleaner to use assignment. Or to be able to create other operators (same way in Scala), for example for “:”.
But making the assignment overloadable is so minor restriction which would give little, but would cause deeply hidden bugs. So I am content with it.
PS: By the way, the assignment (opposite to Java) is not an operator, but a statement. So operator overloading is meaningless.
you can use invoke
instead like this "field" { 1 }
or if you are willing to use reflection:
jsonObject { //inside here you are operating on a JsonObjectScope. Replace that name with whatever you are actually using. It should be the one that defines the += operator
run {
val field by Value(1)
field = "test" // Error! wrong type
field = 5 // Works just fine!
println(field) // Prints 5. Yay!!
}
val field by noValue<Int>
println(field) // Prints 5, too!
}
inline class Value<out T>(val value: T)
sealed class JsonObjectField<out T> {
internal object Impl: JsonObjectField<Nothing>
}
//Note: this assumes that your serializer needs to know the type of the values. If not, then you can remove all of the inlines and reified types and make them normal type parameters
class JsonObjectScope {
...
inline fun <reified T> Value<T>.provideDelegate(thisRef: Any?, prop: KProperty<*>): JsonObjectField<T> {
storeAValueWithAName<T>(prop.name, this.value)
return JsonObjectField.Impl
}
fun <T> noValue(): JsonObjectField<T> = JsonObjectField.Impl
inline fun <reified T> JsonObjectField<T>.getValue(thisRef: Any?, prop: KProperty<*>): T = getValueAssociatedWithTheName<T>(prop.name)
inline fun <reified T> JsonObjectField<T>.setValue(thisRef: Any?, prop: KProperty<*>, value: T) {
storeAValueWithAName<T>(prop.name, value)
}
}
This preserves type-safety and allows the user to reassign the field multiple times to their heart’s pleasure. It uses certain DSL tricks, but the only real incurred overhead is the properties because they require reflection. The rest just works normally with very minor overhead.
Your solution is nice, but my goal with the DSL was to fluently build up JSON using Google’s JsonObject from GSON. There was no need of reassigning the values, so your solution has unnecessary boilercode. I tried to fit as much to the JSON format as possible:
fun jsonObject(obj: JsonObject = JsonObject(), op: JsonObjectBuilder.() -> Unit) = JsonObjectBuilder(obj).apply(op).build()
fun jsonArray(arr: JsonArray = JsonArray(), op: JsonArrayBuilder.() -> Unit) = JsonArrayBuilder(arr).apply(op).build()
fun jsonArray(items: Iterable<Any?>): JsonArray {
val res = JsonArray()
items.forEach { addToArray(res, it) }
return res
}
fun jsonArray(vararg items: Any?): JsonArray {
val res = JsonArray()
items.forEach { addToArray(res, it) }
return res
}
private fun addToArray(res: JsonArray, it: Any?) {
if (it == null) res.add(JsonNull.INSTANCE)
else when (it) {
is String -> res.add(JsonPrimitive(it))
is Number -> res.add(JsonPrimitive(it))
is Boolean -> res.add(JsonPrimitive(it))
is JsonObject -> res.add(it)
is JsonArray -> res.add(it)
else -> res.add(JsonPrimitive(it.toString()))
}
}
class JsonObjectBuilder(private val obj: JsonObject = JsonObject()) {
infix operator fun String.plusAssign(value: Int) = obj.addProperty(this, value)
infix operator fun String.plusAssign(value: Long) = obj.addProperty(this, value)
infix operator fun String.plusAssign(value: Boolean) = obj.addProperty(this, value)
infix operator fun String.plusAssign(value: String) = obj.addProperty(this, value)
infix operator fun String.plusAssign(value: Double) = obj.addProperty(this, value)
infix operator fun String.plusAssign(value: JsonElement) = obj.add(this, value)
infix operator fun String.plusAssign(value: Any?) =
if (value == null)
obj.add(this, JsonNull.INSTANCE)
else obj.addProperty(this, value.toString())
// This allows to write "subobject" { } instead of "subobject" += jsonObject {}, but I rarely use it
operator fun String.invoke(op: (JsonObjectBuilder) -> Unit) = obj.add(this, JsonObjectBuilder().apply(op).build())
fun build() = obj
}
class JsonArrayBuilder(private val arr: JsonArray = JsonArray()) {
fun add(other: Number) = arr.add(JsonPrimitive(other))
operator fun Boolean.unaryPlus() = arr.add(JsonPrimitive(this))
operator fun String.unaryPlus() = arr.add(JsonPrimitive(this))
fun NULL() = arr.add(JsonNull.INSTANCE)
operator fun JsonElement.unaryPlus() = arr.add(this)
operator fun Any?.unaryPlus() = if (this == null) NULL() else arr.add(JsonPrimitive(this.toString()))
fun build() = arr
}
and with it, you can write something like this:
val json : JsonObject = jsonObject {
"alma" += 4
"citrom" += null
"3" += "null"
"o" += jsonObject { // I could write the short form, too: "o" {
"b" += true
}
"arr" += jsonArray {
// One can add any business logic within
(0..10).forEach { +"Value $it" }
+"alma"
+jsonObject { }
}
"a2" += jsonArray(1, null, "lama")
// Call the toJson of subobject
"sub" += subObject.toJson()
}
This also allows extensible JSON building (used in inheritance):
open class Parent( val x : Int ) {
open fun toJson() = jsonObject {
"x" += x
}
}
class Child(x : Int, val y : Boolean) : Parent(x) {
override fun toJson() = jsonObject(super.toJson()) {
"y" += y
}
}
println( Child( 1, true ).toJson() ) // Prints { "x" : 1, "y" : true }
Both your and my solution is correct but aims different goals.
Yep. Alternatively, you can remove almost everything that I had, make getValue
and setValue
no-ops on Unit
, and in the provideDelegate
method you can then just save the value and return Unit.
Then you can write code like this:
val thing by Value(5)
or just val thing by 5
if you make provideDelegate
an extension on Any?
I assume that nobody would deny that it would be nice to have some kind of overloadable assignment operator. I agree that making = overloadable seems dangerous. So let’s talk about alternatives. These come to my mind:
:
:=
Right now Kotlin does not have any overloadable operators, which are not already operators in the language. This would be the first one. And it might be hard to make : an overloadable operator, since it is used in the Kotlin syntax already.
I’m not sure. So far the only use cases I’ve seen are:
- self assignment validation (mostly C++ examples)
- DSL look-and-feel (no new functionality)
The C++ examples seem to short cut self assignment, which I imagine is important if you’re dealing with a raw value instead of a pointer.
The DSL look-and-feel can be solved with properties and extension properties. Even outside of a DSL context, one can always make a property with a defined setter.
Overriding equals moves the responsibility from the owner of the property to the class of that property.
Does this move cover use cases other than look-and-feel and short cutting self assignment?
The =
in your example isn’t really assignment, though, in that it’s not changing the value in an existing “binding” from names to values; rather it’s constructing a “map” (i.e., a JSON object). So the Kotlin way to do this in your DSL would be to override inline fun to
and use to
instead of +=
or =
.
I would understand if you don’t like that Kotlin way, though - many have asked for a map init syntax more like other languages, e.g., allowing :
instead of to
. If that were possible, would that satisfy you?
And I would add all this is predicated that the “operator” should be the = operator but that goes against the semantics of the language since a variable in Kotlin never holds an acual object, it holds a reference to the object. Imagine if you could overload assignment. How could you then change the object that the variable is pointing at.
So if you change it where it isn’t the = operator but instead use a name you could always use an infix function to represent the operation.
But it is really hard to address the idea without a concrete example of how it is expected to work.
I think if you include infix functions, then you could say you can make whatever operator you want. The only restriction being that they have to be valid function names and not just symbols (unless you use the backtick quotes around it). So to me all you are saying is that you would like to create infix functions containing symbols for the name without using backticks, which there could be some value to.
That would probably add a lot of bugs to the kotlin eco system. This feature would mostly be used to create custom operators. The problem with that is that there is no way to specify precedence for those “operators”. I can already see complaints about incorect bitwise operation execution order.
The same problem already exists with infix functions (eg. shl
, or
, and
, etc) but it’s easier to spot since infix functions are obviously not operators.