Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Should structs be immutable? #2362

Closed
Tracked by #2360
leafpetersen opened this issue Jul 29, 2022 · 9 comments
Closed
Tracked by #2360

Should structs be immutable? #2362

leafpetersen opened this issue Jul 29, 2022 · 9 comments
Assignees
Labels
data-classes extension-types inline-classes Cf. language/accepted/future-releases/inline-classes/feature-specification.md structs

Comments

@leafpetersen
Copy link
Member

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 leafpetersen added inline-classes Cf. language/accepted/future-releases/inline-classes/feature-specification.md data-classes structs labels Jul 29, 2022
@leafpetersen leafpetersen self-assigned this Jul 29, 2022
@leafpetersen
Copy link
Member Author

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.

@Wdestroier
Copy link

Wdestroier commented Jul 30, 2022

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.

@lrhn
Copy link
Member

lrhn commented Jul 31, 2022

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.

@mkustermann
Copy link
Member

mkustermann commented Aug 1, 2022

Two more advantages of immutability:

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

@eernstg
Copy link
Member

eernstg commented Aug 1, 2022

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.

@mraleph
Copy link
Member

mraleph commented Aug 2, 2022

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.

@Wdestroier
Copy link

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

@munificent
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
data-classes extension-types inline-classes Cf. language/accepted/future-releases/inline-classes/feature-specification.md structs
Projects
None yet
Development

No branches or pull requests

7 participants