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