const or immutable?

Ali Çehreli acehreli at yahoo.com
Sat Sep 25 00:49:10 UTC 2021


On 9/24/21 10:20 AM, H. S. Teoh wrote:

 >> tl;dr Do you use 'const' or 'immutable' by-default for parameters and
 >> for local data?
 >
 > Nope.  Probably should, but I don't because the transitivity of
 > const/immutable makes it very difficult to use correctly in complex data
 > structures.

That's disconcerting and matches my experience as well.

 > So IMHO, not worth the effort.

I am going there as well. However, you seem to have some guidelines, 
which makes me think you take advantage of immutability as long as it is 
practical.

 > Where I've had most success with const/immutable is with leaf-node
 > modules implementing simple data structures, i.e., at the bottom of the
 > dependency chain, where other code depends on it but it doesn't depend
 > on other code. And perhaps one or two levels above that. But beyond
 > that, it quickly becomes cumbersome and with benefits not proportional
 > to the amount of effort required to make it work.

That answers one of my questions: There are cases where some local 
variables cannot be const or immutable because they will inevitably 
passed to high-level functions.

 > Use const when the data could be either immutable or
 > mutable (usually this applies to function parameters),

That's the simplest case to agree on: Yes, function parameters should be 
'const'.

 > immutable everywhere else.

This matches what I currently have in the book, which bugs me a little 
bit. (Note the following example comes with a bonus minor and off-topic 
issue, which may be related to your complaints about complexity 
hindering immutability.)

```
import std.algorithm;
import std.range;

void foo(const(int)[] input) {
   immutable halfLength = input.length / 2;
   immutable processed = input.map!(i => immutable(int)(i * i)).array;

   // ...
}

void main() {
   foo([ 1, 2 ]);
}
```

Main issue: Ok, local variables are defined as immutable there. However, 
I see that as being superfluous (presumptuous?) because I definitely did 
not mean "*others* may not mutate it please". All I meant is immutable. 
So, using immutable carries that extra meaning, which is not really used 
there.

<off-topic>
Bonus minor and off-topic issue: I think we have an issue in the type 
system. If we replace `immutable(int)(i * i)` with just `i * i`, we get 
the following compilation error:

Error: cannot implicitly convert expression `array(map(input))` of type 
`const(int)[]` to `immutable(int[])`

The problem is, `i * i` has no indirections and is starting life afresh. 
It can be `immutable`. But I understand how the type of the right-hand 
side is separate from the type of the left-hand side. Let's try casting 
the right-hand side:

   auto processed = cast(immutable)input.map!(i => i * i).array;

Ok, it works! :) (And of course, `auto` does not mean mutable there.)

More off-topic: Today, I learned that the type of a vector in Rust is 
determined at first use. (Or something like that.) So, if `auto` were 
legal there in D, it would be demonstarted like the following in D:

   auto[] arr;    // No type assigned yet
   // ...
   arr ~= 42;     // Ok, the type of arr is int[]

Still statically-typed... Too flexible for D? Perhaps not because we 
have something similar already, which I think Rust does not have: The 
return type of an auto function is determined by the first return 
statement in D.
</off-topic>

 > I think it's fallacious to try to put const/immutable on a scale of
 > "better" or "worse".

Like you and others, I agree and I tried to convey that point.

 > Parameters are where the const/immutable are most differentiated. Local
 > data -- it depends. If you got it from another function call, it may
 > already come as const or immutable, so you just have to follow what you
 > receive (or weaken immutable to const if you wish, though I don't see
 > the point unless you plan to rebind it to mutable later on).

Unfortunately, intermediate functions do weaken `immutable` in the name 
of being "more useful". (This is related to my "functions should be 
templates" comment below.

void main() {
   immutable a = [ 1, 2, 3 ];

   // Passes the array through foo() and bar()
   immutable b = foo(a);
   // Error: cannot implicitly convert expression
   // `foo(cast(const(int)[])a)` of type
   // `const(int)[]` to `immutable(int[])`
}

auto foo(const(int)[] arr) {
   return bar(arr);
}

auto bar(T)(T arr) {
   return arr;
}

The code compiles if foo() is a template because in that case the type 
is preserved as `immutable(int)[]`.

Off-topic: I am sure you are aware of the magic that happened there. The 
types are *usefully* different:

immutable(int[]) --> The type of 'a' in main.
immutable(int)[] --> The type of 'arr' parameter of foo().

 > If it's pure POD locally-initialized, just use immutable.

Ok, I used `immutable` for non-POD and some strange thing happened. :)

 > In this case, I'd use `in` instead: this tells the caller "this
 > parameter is an input; its value will not change afterwards". For PODs,
 > it already doesn't change, but `in` reads nicer than `const`. :-D

I agree. I have many examples in the book where parameters are 'in'. But 
I don't have a single 'in' keyword in my code at work. :( I will start 
taking advantage of --preview=in.

 >> void printFirstChar(const char[] s) {
 >>    write(s.front);
 >> }
 >>
 >> But wait! It works above only because 'front' happened to work there.
 >> The problem is, 's' is not an input range; and that may matter
 >> elsewhere:
 >>
 >>    static assert(isInputRange!(typeof(s)));  // Fails. :(
 >
 > It makes me cringe everytime someone writes const/immutable without
 > parentheses, because it's AMBIGUOUS, and that's precisely the problem
 > here.  What you *really* meant to write is const(char)[], but by
 > omitting the parentheses you accidentally defaulted to const(char[]),
 > which no longer works as a range.

Yes but confusingly `s.front` works.

 > Also, const(char)[] works as long as you don't need to rebind. But if
 > you do, e.g., in a parsing function that advances the range based on
 > what was consumed, you run into trouble:
 >
 > 	// N.B.: ref because on exit, we update input to after the
 > 	// token.
 > 	void consumeOneToken(ref const(char)[] input) {
 > 		...
 > 	}
 >
 > 	string input = "...";
 > 	consumeOneToken(input);	// Oops, compile error
 >
 > On the surface, this seems like a bug, because input can be rebound
 > without violating immutable (e.g., input = input[1..$] is valid). But on
 > closer inspection, this is not always true:
 >
 > 	void evilFunction(ref const(char)[] input) {
 > 		char[] immaMutable;
 > 		input = immaMutable; // muahaha
 > 	}
 >
 > 	string input = "...";
 > 	evilFunction(input); // oops, we just bound mutable to immutable

This is an issue I am aware of from C, which manifests itself in two 
levels of pointer indirection. (I can't construct an example but I 
recognize it when I hit the issue. :) )

 >> (Granted, why is it 'const' anyway? Shouldn't printFirstChar be a
 >> function template? Yes, it should.)
 >
 > Why should it be?  Using a template here generates bloat for no good
 > reason. Using const(char)[] makes it work for either case with just one
 > function in the executable.

The example above is related to this issue. But I admit: These are not 
cases we face every day.

 > Also, const for local variables are really only necessary if you got the
 > data from a function that returns const; otherwise, it's practically no
 > different from immutable and you might as well use immutable for
 > stronger guarantees.

That answers a question but as I said above, `immutable` carries extra 
meaning and it's longer. ;)

 > Immutable is powerful because it has very strong guarantees.
 > Unfortunately, these very strong guarantees also make its scope of
 > applicability extremely narrow -- so narrow that you rarely need to use
 > it. :-D

Agreed. For example, I use `immutable` when I pass data between threads. 
(And implicitly, every time I use `string`. ;) )

Ali



More information about the Digitalmars-d mailing list