language icon indicating copy to clipboard operation
language copied to clipboard

Should structs be immutable?

Open leafpetersen opened this issue 1 year ago • 9 comments

The struct proposal requires that all fields in a struct/data class be final. The motivations around this are primarily around representation: one of the problems that I am aiming to solve is that we have a demonstrated need for object representations that allow the compiler to freely do scalar replacement of aggregates/unboxing optimizations that do not preserve identity (I believe that our SIMD classes already do this, in mild violation of the specified behavior). A secondary motivation is around brevity: based on the user feedback we've received around data classes, by far the common case that people wish to support is immutable data. Specifying that all fields in a struct are immutable allows the briefed int x field declaration form to be used instead of the more verbose final int x form.

Is this the right choice?

On the semantic end, allowing mutable fields + unboxing is very questionable. Aliasing of mutable objects must be predictable: if the compiler is free to unbox and re-materialize a single mutable object, chaos ensues. There are three possible solutions I see to this:

  • Say that any struct with a mutable field must preserve object identity (but fully immutable structs do not)
  • Say that mutable fields in a struct must be represented via an additional indirection which preserves identity.
  • Say that all structs must preserve identity

On the brevity end, I see (at least) three choices:

  • Say that all fields in a struct are mutable by default and require final fields to be marked as usual in the primary constructor
  • Say that all fields in a struct are immutable by default, but allow fields to be marked mutable with var (e.g. var int x)
  • Say that fields declared in the primary constructor are immutable, but allow mutable by default fields to be declared in the body of the struct.

leafpetersen avatar Jul 29 '22 22:07 leafpetersen

cc @mit-mit @lrhn @eernstg @chloestefantsova @johnniwinther @munificent @stereotype441 @natebosch @jakemac53 @rakudrama @srujzs @sigmundch @rileyporter @mraleph @mkustermann

leafpetersen avatar Jul 30 '22 00:07 leafpetersen

In general, my take on this is that WRT the semantic aspect, I don't like the idea of adding an indirection, and I think there is real value in having a variant of classes with structural identity. This leaves the idea of requiring identity to be preserved only for structs with mutable fields, which I think is ok, so long as there is no way to subclass/implement a struct and add in a mutable field. It does feel a bit odd though.

On the user facing end, my worry with making the user mark final variables is that we start down a path to death by 1000 cuts. This proposal is trying to be highly opinionated, to get as much value for the common case as possible. Allowing mutable variables to be explicitly marked with var is an odd departure from everywhere else in the language. And having int x mean a different thing when declared in the primary constructor vs in the body seems very surprising, and potentially a foot gun.

leafpetersen avatar Jul 30 '22 00:07 leafpetersen

In the Overview of a proposal for structs document summary it says:

The desire for a zero cost wrapper type (motivated largely by interop concerns)

I think structs shouldn't be immutable if C/C++ interoperability is desired. Imagine the scenario where a native library allocates a struct and Dart FFI returns a Dart struct backed by the same memory address. If the struct stores an int64 or a pointer address, then these values cannot be changed and may not be possible to change references in the native library to the new struct with the desired values.

Wdestroier avatar Jul 30 '22 23:07 Wdestroier

If we allow modification of member variables of a struct, we effectively prohibit some variants of unboxing.

Objects have consistent identity, and that identity is preserved across assignment.

C++ structs have identity, but copy semantics for assignment (and pointers/references for the cases where you want to share/alias a single identity).

To meaningfully copy C++ behavior for Dart structs (which is not necessarily a goal), Dart would need to have copy semantics too.

We could say that structs have identity when stored in a variable, and all accesses to the struct through the same variable accesses the same struct instance, but that assigning to another variable (including parameter passing and returning) does not need to preserve identity. That's just not enough to allow the struct to be modifiable. You need to be able to predict precisely when a new instance is created, so you don't accidentally change another instance through unexpected aliasing. So, allowing modification means we cannot share the struct, but must copy it on each assignment. Even if it isn't unboxed, which will be costly in allocation (although I guess we can do copy-on-write optimizations).

It also goes the other way, we cannot make copies prematurely, which may prevent some unboxing. We can unbox a struct, but we must ensure that there are no other references to the same struct that are still alive, because then we won't preserve identity properly. Unboxing is copying. If we can prove that there is only a single boxed or unboxed version of a struct at any given time, then it's safe to allow modification. If we can't, we cannot unbox.

Allowing modification risks blocking a lot of potentially valuable optimizations. It requires specifying a precise and predictable identity strategy that all implementations must follow.

I'd prefer immutability, because it's extremely predictable.

lrhn avatar Jul 31 '22 10:07 lrhn

Two more advantages of immutability:

(By statically seeing that a struct's members are either structs or primitives, thereby making them transitively immutable)

mkustermann avatar Aug 01 '22 08:08 mkustermann

I would very much support having immutable struct entities, with no guaranteed identity, and supporting structural comparison (presumably by means of an operator == whose implementation is provided automatically, with the semantics described in the proposal).

As mentioned several times already, mutability implies identity, in the sense that it's impossible to write correct programs when two references to a mutable entity may or may not be aliases to the same entity, and you cannot know which. So if we wish to allow the compiler to decide when to box/unbox a struct entity then we have no choice.

We could also use a C++ like semantics, and require that developers specify exactly how to manage the storage of struct entities. In that case they could be mutable. However, I do not think this kind of low-level memory management is a good match for Dart. I'd very much prefer to have a clear and safe semantics, and leave ample opportunities for compilers to optimize this kind of code heavily.

eernstg avatar Aug 01 '22 14:08 eernstg

I think struct's should be immutable (all fields implicitly final). "Updating" structs which appear within mutable classes is supported by the f = f.copyWith(field: newValue) pattern (which compilers are free to optimise). That leaves a question how deeply nested structs are updated, e.g. if you have

struct A(int x);
struct B(int y, A a);

B b;

How do you do an equivalent of b.a.x++? b = b.copyWith(a: b.a.copyWith(x: b.a.x + 1)) is a mouth-full.

It's hard to predict how often this would occur but it would be interesting to consider some variation of recursive record update syntax: b = b { a: { x: x + 1 } }. { x: expr, ... } specifying an update of x to expr and the current value of x brought into scope automatically.

@Wdestroier the concept of struct in the context of this proposal is unrelated to FFI (C) structs.

mraleph avatar Aug 02 '22 11:08 mraleph

@mraleph Thanks 😊, just to be clear I'm fine with any decision about this...

Wdestroier avatar Aug 02 '22 13:08 Wdestroier

Yes, I think structs should be immutable. If you want a "mutable struct", what you probably really want is just a class with the brevity of a primary constructor. We should let you use primary constructors on classes too.

munificent avatar Aug 04 '22 22:08 munificent