Inline classes: 'var' to be deprecated?

Personally, I don’t think the concerns about optimization will be much of an issue–there’s a lot of factors and it’s easy to get caught up in pre-optimizing.

For those who haven’t, I highly recommend reading the KEEP cover to cover–it’s a fun read. Looking at this section about mutable properties, this code would no longer compile and is the reason for the concern:

val grid = Grid(ptr = 0L) // Notice it is a val
grid.someVarProperty = 42 // ERROR, this is a mutating setter (wither) but grid is Val.

My current favorite is not inventing a new keyword and just reusing var to mean mutating setter (wither).

If that was the final design these use-cases would look like this:

val grid = Grid(ptr = 0L) // Notice it is a val
grid.someFunction(42) // None mutating function.

The optimization isn’t all that relevant, beyond trying to keep value classes cache friendly and avoid allocations, as per their intent in Valhalla. I’m assuming that’s a large motivation for incorporating them in Kotlin.

Here’s a pretty realistic example of what I’d like to support:

// With setters:
component.velocity.y -= gravity * dt
component.health += complexMathExpression
networking.status = NetworkStatus.DISCONNECT
framework.run { entity.gun.scale *= 2f } // Here gun is an extension property defined in framework, entity is just an index

// Without setters:
component.velocity.setY(component.velocity.y - gravity * dt)
component.setHealth(component.health + complexMathExpression)
networking.setStatus(NetworkStatus.DISCONNECT) // Not as bad
framework.run { entity.gun.setScale(entity.gun.scale * 2f) }

As you can see, the loss of compound assignment is the real issue. Regular assignment isn’t as pretty, but it’s much more workable. But considering that incremental updates are the norm in game logic, this would be a serious problem. I have a much harder time reading the second version, and it’ll affect anyone using an ECS framework, “Unsafe” code or not.

Here’s what I’m currently proposing as a solution:

value class Foo(...) {
    // No changes to these:
    val x = 0
    var y = 1f
    val z get() = 2.0

    // Or these:
    fun myFunc1() = ...
    mutating fun myFunc2() = ...

    // Allow custom setter, assume "mutating", don't require additional syntax...
    var a: Float
        get() = ...
        set(v) { ... }

    // ...but allow 'const' to specify "non-mutating", working as inline classes currently do:
    var b: Float
        get() = ...
        const set(v) { ... }
}

The reason for using const is A) no new keyword, and B) not requiring a mutating prefix on the other setter, since I imagine it would be by far the most common case.

2 Likes

What are people using this word to mean here? It seems to have nothing to do with the usual meanings (to shrink, weaken, wrinkle, deteriorate; to humiliate).

A wither, as opposed to a setter, is a mutating function that changes a property in a clone of the receiver and returns the clone instead of mutating the property of the receiver. It is used in the context of immutable classes.

The word itself comes from the fact that such functions are often named something like withX.

class Foo(val bar: String, val baz: String) {
   fun withBar(bar: String) = Foo(bar, baz)
}

This thread discusses a potential future addition to Kotlin inline classes (immutable value classes) which would have var properties that are syntactic sugar for a wither function.

3 Likes

The name wither is confusing. Normally with means to do something with the current object. There is lots of real code that use with in this way.

I think it is more of a copier like the data classes have.

val grid = Grid(ptr = 0L)

// Normal property just like classes that are not value classes,
// i.e. they can do anything.
// Does not mean they mutate the state passed to the constructor.
grid.someVarProperty = 42

// I do not see a use case for this as there is only 1 actual
// value in a value class.
grid.copy(someVarProperty = 42)

In my opinion you get confusing code if, for value classes, you change var to mean: make a copy of this object. Setters on normal classes do not make copies of objects.

In most cases value classes will not have var properties, I suspect. If they do, they are likely to use the value class as a proxy for some actual value (as shown in the original example). This is normal object behavior: the object you call/set a property value on forwards it to an underlying object.

1 Like

Actually I just realised something. Assuming that you use a normal mutating var, wouldn’t the class just have to do a “copy” on the underlying long value? Which in bytecode would just be it returning the same exact value again? To be honest, this seems like there won’t even be a trade-off, well maybe except that every single Grid that wants to use those extension vars would have to be a var itself, which maybe could be problematic if you don’t want your Grid pointer to ever change, but that could easily be replaced with a no-op setter, right? Please correct me if I’m wrong. You could even create a delegate that just does absolutely no setting whatsoever and use that for it.

@kyay10 I believe that is correct. The workaround is to treat the noop setters as standard withers and the long value will be copied (or optimized out), callers will be forced to use var without the promise that the value is a noop or non-mutating setter.

@jstuyts IMO wither is a nice practical name. It’s already convention to make mutating copy-like functions in the form immutable.withXXX(value). Alternatives like “copier” is less unique and a bit ambiguous–I haven’t seen a conflicting use of “wither” as a noun.

You are correct. I highly recommend reading through the KEEP in its entirety. Mutating functions and withers help one work with immutable objects. Not having them in some form would be difficult. And having non-mutating setters on immutable data is arguably an edge case (like OP’s case).

2 Likes

It would be against the concept of mutating var that it should not destroy the original value. I mean in the following code:

val a = ValueClass(property = 1)
var b = a
b.property = 2
// after this point two invariants must be hold if 'property' is a mutating var:
a != b
a.property == 1

it won’t be possible if property setter mutated an underlying object and returned it instead of returning a modified copy.

2 Likes

Compare the code above with the following:

val a = NormalClass(property = 1)
var b = a
b.property = 2 // "property" must be a var
// After this point the following holds:
a == b
a.property == 2

The exact same code except for 1 identifier, and totally different semantics. Is that the direction Kotlin should be going?

  • How much extra time and mental effort has to be spend to check the class type of variables?
  • How many code changes would be needed if you decide to change from a class type to another?

Value classes look more like data classes to me:

val a = ValueClass(property = 1)
var b = a
b = b.copy(property = 2)
// Now the desired invariants hold:
a != b
a.property = 1

And the above code still works the same for normal and data classes.

Of course, better syntax is likely possible.

Note: I still have not read the KEEP.

2 Likes

I was just thinking the same thing!

Perhaps something like ‘copy-setter’ would be clearer? (On the same lines as the copy() method in data classes, and the general term ‘copy-constructor’.)

I’ve been thinking about the case of var properties with custom setters having a backing field. In that case, this may be extremely unintuitive:

value class Bar(var x: Int)

value class Foo(bar: Bar) {
    var bar = bar; set(v) { field = v; /* Do a bunch of other stuff */ }
}

var foo = Foo(Bar(0))
foo.bar.x = -1 // Does "a bunch of other stuff" occur?

If the setter/wither gets invoked, then this violates the notion of “working with immutable types being inherently safe” imo. However, if we’re treating this as a copy constructor instead, then there would be no side effect.

What is the best behavior here?

  1. Disallow
  2. Copy the field without invoking the setter
  3. Invoke the setter

On another issue, pertaining to what @kyay10 was describing: for non-mutating custom setters, is there a reason to disallow their calling on val instances? It’d be analagous to calling any other non-mutating function, just different syntactically.

Also, I haven’t mentioned it yet, but I really appreciate JetBrains taking the time to listen to our feedback and make changes accordingly. You guys are awesome! And I’m sure that has a lot to do with why Kotlin is such an incredible language.

Hopefully you will forgive our skimming of documentation and lobbying for pet features long enough to glean something of value from our discussion. (cough, that’s not just me right? cough)

6 Likes

Hello. Sorry if I missed something, but why it’s not possible to use extension properties, like in the following example?


inline class Grid(val ptr: Long)

var Grid.width: Int
    get() = GridNative.getWidth(ptr)
    set(v) { GridNative.setWidth(ptr, v) }

What would converting to an extension property achieve? My request essentially boiled down to allowing the case of a custom setter that is non-mutating (i.e. no backing field), whereas previously all custom setters were on the chopping block. Were extension var setters not also on the chopping block? What would the justification be for allowing one and not the other? Correct me if I’ve misunderstood.

If extension properties are not withers, then I would think that extension properties is nice solution for this use-case.
It fits conceptually as well since value-class properties with setters must be either withers or must behave like any normal extension property setter–making these kinds on non-wither setters always be extensions makes sense.

However, if extension properties are withers, you won’t be able to use them with a val reference.

One potential trade-off: Making all side-effect setters (non-wither) only be allowed as extension properties is great, but does that mean you won’t be able to define an extension wither? I imagine there are lots of use-cases for extension withers.

hmmm, could their just be a very simple nonmutating modifier for setters maybe? Kinda like open vs final where, depending on the context, those modifiers are assumed and can be overwritten using the opposite one.

I found a conceptual problem in my solution: in case of such extension property it’d not be possible to make the underlying property private — that violates encapsulation :frowning:

Kotlin allows us to make a “mutable” property on a final class:

var String.myVaryingProperty: String // please never write such code :)
    get() = System.getProperty(this)
    set(value) = doSomething(this, value)

fun doSomething(key: String, value: String) {
    println("$key -> $value")
}

object MiniTest {
  @JvmStatic
  fun main(args: Array<String>) {
    "theKey".myVaryingProperty = "theValue"
  }
}

That’s allows to overpass the restriction of vars inside inline class.
However, I’ve already found my solution not good because it requires to public the underlying property.

1 Like

Rather than use a system property, you could have a private WeakHashMap, with the instance as the key.

I think that’s a fairly standard way to associate extra info with an object you don’t control.⠀Because it uses weak references, it doesn’t prevent the objects being garbage-collected, and you can add whatever info you want as the value.

Of course, there are potential issues if used from multiple threads; you could wrap it in a Collections.synchronizedMap() for safety (or see this question for higher-performance approaches).

Value classes miss of identity, so weak map does not work as expected.