mutable, const, immutable guidelines

H. S. Teoh hsteoh at quickfur.ath.cx
Wed Oct 16 12:46:28 PDT 2013


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.


More information about the Digitalmars-d-learn mailing list