On Wed, Oct 16, 2013 at 09:06:05PM +0200, Daniel Davidson wrote: > On Wednesday, 16 October 2013 at 18:52:23 UTC, qznc wrote: [...] > >Library code: > > > >struct Foo { int x; } > > > >User code: > > > >Foo f; > >immutable f2 = f; > > > >This works, even though the library writer might not have > >anticipated that someone makes Foo immutable. However, now the > >library writer obliviously releases a new version of the library, > >which extends it like this: > > > >struct Foo { > > int x; > > private int[] history; > >} > > > >Unfortunately, now the user code is broken due to the freshly > >introduced mutable aliasing. Personally, I think is fine. Upon > >compilation the user code gives a error message and user > >developer can adapt to the code to the new library version. Some > >think the library writer should have a possibility to make this > >work. > > I don't understand how it could be fine. As code grows it would lead > to people not adding useful members like history just because of the > huge repercussions. > > struct User { > immutable(Foo) foos; > } > > How can I as a user adapt to that change? Before the change > assignment worked equally well among all of Mutable, Immutable, > Const. After that change any `foos ~= createFoo(...)` would require > change. And it is not clear what the change would be.
The root of the problem is reliance on assignment between mutable / immutable / const. This reliance breaks encapsulation because you're making an assumption about the assignability of a presumedly opaque library type to immutable / const. In D, immutable is *physical* immutability, not logical immutability; by writing immutable(Foo) you're saying that you wish to have physically-immutable instances of Foo. However, whether this is possible depends on the implementation details of Foo, which, if Foo is supposed to be an opaque type, breaks encapsulation. Without knowing how Foo is implemented (and user code shouldn't know that), you can't reliably go around and claim Foo can be made immutable from a mutable instance. The fact that you're relying on Foo being implicitly convertible to immutable(Foo) means you're already depending on implementation details of Foo, and should be prepared to change code when Foo's implementation changes. If you want to say that User cannot modify the Foo's it contains, you should use const rather than immutable. It is safe to use const because anything is implicitly convertible to const, so it doesn't introduce any reliance upon implementational details of Foo. If you insist on being able to append to immutable(Foo)[], then you'll need a createFoo method that returns immutable instances of Foo: struct User { immutable(Foo)[] foos; } immutable(Foo) createFoo(...) { ... } User u; u.foos ~= createFoo(...); // now this works The problem with this, of course, is that it unnecessarily restricts createFoo(): if you want *mutable* instances of Foo, then you can't use this version of createFoo(), but have to create another function that probably does exactly the same thing. So an alternative solution is to use Phobos' assumeUnique template: struct User { immutable(Foo)[] foos; } Foo createFoo(...) { ... } User u; u.foos ~= assumeUnique(createFoo(...)); The assumeUnique template basically does a cast from mutable to immutable, but explicitly documents the purpose of this cast in the code. It places the onus on the user to ensure that the Foo returned by createFoo is actually unique. If not, you break the type system and the immutability guarantee may no longer hold. To illustrate why adding mutable aliases to Foo *should* break code, consider this: /* This is what Foo looked like before: struct OriginalFoo { int x; } */ /* This is what Foo looks like now */ struct Foo { int x; private int[] history; void changeHistory() { history[0]++; } } Foo createFoo(int x) { Foo f; f.x = x; f.history = [1]; } Foo f = createFoo(); immutable(Foo) g = f; // doesn't compile, but suppose it does f.changeHistory(); // oops, g.history has mutated, so it's // *not* immutable after all That's why assigning f to g must be made illegal, since it breaks immutability guarantees. OTOH, if you absolutely have to do it, you can document your intent thus: Foo f = createFoo(); immutable(Foo) g = assumeUnique(f); // Now if you use f to mutate g, it's your own problem: you // claimed that g was unique but actually it isn't. So it's your // own fault when your supposedly-immutable Foo mutates. // If you *don't* do stupid things, OTOH, this lets your code // continue to work when the library writer decides to change // Foo's implementation to contain mutable aliases. T -- Do not reason with the unreasonable; you lose by definition.