The inheritance + immutability combination forces the compiler to use field-by-field copying rather than constructor chaining, which bypasses the property initialization logic that would maintain consistency between related fields.
Cloning anything creates a new object of a known type (well, the runtime knows at least) and so if the object re-runs the init-properties of the known type then it will be the same as constructing that type afresh.
You could even imagine a compiler generated virtual method: `OnCloneReinitialiseFields()`, or something, that just re-ran the init-property setters (post clone operation).
Is there some other inheritance issue that is problematic here? Immutability isn't a concern, it's purely about what happens after cloning an object, whether the fields are immutable or not doesn't change the behaviour of the `with` operation.
It was never immutable? You can have collections on there that are perfectly mutable as well as being able to set values?
You CAN use the with keyword, but that's a choice.
I've seen people use records for value based equality and to use for things like dictionary keys. Immutability in c# just doesn't exist, any attempt to achieve it is flawed from the start
ooops, my bad.
I remember, that Aleksey Shipilëv explained why even `final static` fields are not constant-folded by JIT, and thought that record classes which were introduced later, is the same.
It means, that `StableValue<>` can be used in simple classes (where `final` fields are still not constant-folded) and, additionally, supports late initialization.
Yeah the Java team took the opportunity while introducing the new record feature to explicitly forbid mutating their final fields. Their intention is to eventually enforce the same invariants across all Java fields unless the JVM is passed an explicit opt-out flag. And if you're not reliant on any frameworks using reflection to modify final fields, there's already a flag you can use today which will instruct the JVM to "trust" and apply optimizations to `final`.
No, that's a mistake in the article. The variable is still captured by reference, but `let` is causing it to be re-declared on every iteration of the loop, not mutated.
The following code prints 1, 2, 3. It wouldn't do that if the variable was captured by value.
for (let i = 0; i < 3;) {
setTimeout(() => console.log(i));
i++;
}
The behavior of "let" with for loops where the variable is declared more times than it is initialized, despite the source code having one declaration that is also the only initialization, is not very explicit.
for (let i=0;i<3;i++) {
i+=10;
setTimeout(_=>console.log(i),30);
i-=10
}
Capture by value would print 10, 11, 12 that's the value when it was captured
Capture by reference would print 0,1,2
It's much easier to conceptualise it as
for (const i=0;i<3;i++) {
setTimeout(_=>console.log(i),30);
}
which is fine because i never changes. It is a different i each time.
fancier example
for (let x = 0, y = 0; y < 2; x=x++<3?x:y++,0){
x+=10;
y+=10;
console.log("inline:",x,y);
setTimeout(_=>console.log("timeout",x,y),30);
x-=10;
y-=10;
}
Parroting something i have heard at a Java conference several years ago, tail recursion remove stack frames but the security model is based on stack frames, so it has to be a JVM optimization, not a compiler optimization.
I've no idea if this fact still holds when the security manager will be removed.
The security manager was removed (well, “permanently disabled”) in Java 24. As you note, the permissions available at any given point can depend on the permissions of the code on the stack, and TCO affects this. Removal of the SM thus removes one impediment to TCO.
However, there are other things still in the platform for which stack frames are significant. These are referred to as “caller sensitive” methods. An example is Class.forName(). This looks up the given name in the classloader of the class that contains the calling code. If the stack frames were shifted around by TCO, this might cause Class.forName() to use the wrong classloader.
No doubt there are ways to overcome this — the JVM does inlining after all — but there’s work to be done and problems to be solved.
There are similarities in the problems, but there are also fundamental differences. With inlining, the JVM can always decide to deoptimize and back out the inlining without affecting the correctness of the result. But it can't do that with tail calls without exposting the program to a risk of StackOverflowError.
We've been using TCO here ("tail call optimization") but I recall Guy Steele advocating for calling this feature TCE ("elimination") because programs can rely on TCE for correctness.
For me, this is where lies the design flaw, trying to support both inheritance and be immutability at the same time.