On Wednesday, 22 September 2021 at 20:06:59 UTC, Ali Çehreli wrote:
>tl;dr Do you use 'const' or 'immutable' by-default for parameters and for local data?
Short answer: immutable
is a special case that is way too prominent in D. const
is my go-to when I need it.
- It is said that 'immutable' is a stronger type of const, which at first sounds great because if 'const' is good, 'immutable' should be even better, right? Unfortunately, we can't make everything 'immutable' because 'const' and 'immutable' have very different meanings at least in parameter lists.
I think we should not fall for the fallacy of "more attributes == good".
More attributes just means more restrictions. Why do we need more restrictions ?
To make our life easier when writing, reading, or changing code. The less a piece of code can do, the easier it is to understand.
I think that fallacy started with @safe
: It is good to make everything @safe
, right ? Can't go wrong with memory safety.
And pure
also, because not using global is good, right ? Except it starts to be very impractical for pure
, so we have hacks like save-set-reset errno
to accommodate for it, because we want things to be pure
, although they mutate global state.
And then enters nothrow
, and @nogc
, and those categorically shouldn't be treated the same as @safe
. Not everything needs to be nothrow
/ @nogc
. And not everything needs to be constified.
I am seeking simple guidelines like C++'s "make everything const."
I don't think it's that simple.
>Aside: If 'const' is welcoming, why do we type 'string' for string parameters when we don't actually require immutable:
This is IMO "D's greatest mistake". When Sociomantic did the D1 -> D2 transition, we came up with three aliases:
alias mstring = char[];
alias cstring = const(char)[];
alias istring = immutable(char)[];
Our guidelines were simple:
- Template Parameter ? =>
istring
; - Mutable buffer =>
mstring
(usuallyref
too as we usedassumeSafeAppend
a lot); - Otherwise, it's most likely
cstring
;
The few exceptions I can remember were some constructors / setters for things that we knew would be constructed once per application lifetime (e.g. command-line parser).
Obviously, cstring
ended up being used in most places. It's now one of the first alias I define whenever I start a project that will deal with strings.
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:
IMO that's another problem. Ranges generally don't work (well) with const
(or immutable
) qualified elements, nor do they work well with non-copyable data.
Or, should 'immutable' be preferable here because it's implicitly shared, which may be useful in the future?
Are there simple guidelines around this topic? Please? :)
Personally, I generally ignore 'immutable' (except, implicitly in 'string') both for parameters and local data.
If we go back to those keywords definition:
const
: Cannot be modified through this instance;immutable
: Cannot be modified through any instance;
Now there's a very annoying (not so) corner case with the second definition: It binds the lifetime of the memory to the lifetime of any instance. Because if the data is destroyed, it means an instance can modify it, which contradicts immutable
. Luckily, D has a GC, meaning we get away pretty easily with this... Until we want every part to function independently and then we don't.
This was an intractable problem before scope
had any effect, because you could stack-allocate an immutable
array and they slice it, and boom! Now the only problem is that we need to combine immutable and smart pointers, so that we can properly free the memory whenever the last reference goes out of scope (remember AfixAllocator and Andrei's talk?).
But I am digressing from your original question. Want a simple set of guidelines ?
Here's what we do:
- Enable
-preview=in
(with DMD >= v2.095) and pass parameters byin
for functions (even for types without indirections); - Avoid
in
for parameters which are stored (setters, ctors...); - Use
const
for parameter with indirections if possible; - Use
immutable
if needed (e.g. need to forward to a function that usesimmutable
); - Use mutable parameters otherwise;
- For local data, don't make it
const
orimmutable
unless needed: If a function is so long that it doesn't fit on one's screen, break it down;
It's simple, it works, it looks good.