September 23, 2021
On 9/22/21 2:34 PM, Kagamin wrote:
> My guideline is that most stuff is mutable, so there's little reason to
> try to make it const.

I've been putting const as much as possible:

  const arr = foo();

However, as soon as I decide to mutate the result, I change it to auto:

  auto arr = foo();
  arr[0] += 42;

So, defensive programming prescribes 'const' and it seems like I did protect myself from accidental mutation. But how common are these accidental mutations? Is it so common to worth the neural activity and needing to change the code when it's not an accidental mutation?

As I hinted in some of my DConf presentations, I started to question the value of some programming constructs. For example, I don't use 'private' unless a type gets complicated enough to protect its invariants.

I agree that 'private' is useful for API types to change implementations freely but I still question: If I already documented how to use the API, even with examples; but the users went ahead and reached inside and used members of my API structs... I am in the opinion that if I still have the right to change the implementation. I don't think it's the end of the world if the users have to change their code.

> For most simple types like scalars and arrays
> const is relatively cheap and useful, but its usefulness decreases with
> complexity of the type, the more complex is the type the more likely
> it's mutable. Strings often benefit from immutability.

There are cases where I remove a 'const' just to make the code compile instead of fighting the type system and it just works.

The fact that I am embarrassed to write these must be telling: Either I am becomming more and more mediocre as a programmer (as I gain more experience!), or maybe there is something else here. :)

Ali

September 23, 2021
On 9/22/21 6:29 PM, Mathias LANG wrote:

> I think we should not fall for the fallacy of "more attributes == good".

In addition to what you say, complexity in a tool is detrimental for adoption, usability, undestandability, maintenance, etc.

> I don't think it's [that simple](https://youtu.be/qx22oxlQmKc?t=923).

Excellent presentation. I have always respected Herb Sutter, am jealous of his methodical approach to problems, and happy to have met him at SD 2000 to chat briefly while having his book signed.

Although most of what he presents is either already in D or discussed for D, he (and many other great minds) are still working for improving C++. I don't approve his ignoring D in the context of that talk; he lists just two of more popular languages (C# and Ada). If his proposal is ever finds its way into C++, it will be again "other languages are taking from us but they never acknowledge it" (what I remember from past a Bjarne Stroustrup keynote speech). It takes a different kind of personality to realize that all improvements that one is thinking for C++ is already in D and to see it reason enough to put efforts in. (I know Herb Sutter knows about D because he has been rubbing shoulders with Andrei. He was a co-author of a proposal to add D's 'static if' for C++, where D was referenced.)

I like how he says "I can't teach it. It's too complicated." Exactly! That's why I am here on this thread.

Some of what he presented relates to D:

- in, out, and ref

- Some of Walter's efforts for @live (when proposed improved C++ compilers analyzing flow graphs)

- In a D constructor, initialization is "first assignment".

- " = void"

> Our guidelines were simple:
> - Template Parameter ? => `istring`;

Of course, manifest constants (e.g. enum) as well.

> - `const`: Cannot be modified through this instance;
> - `immutable`: Cannot be modified through *any* instance;

Allow me to stay on an unnecessary detail: Those are very common definitions that are repeated elsewhere in this thread. However, partly encouraged by Herb Sutter's stressing "intents", I will explain my different take on it: Parameters are parts of a contract between the function and the caller; they must be described from the function's point of view:

- When a function parameter is 'const', it is the function's telling to the caller "I promise I will not mutate it". (And of course the compiler enforces it.)

- When a function parameter is 'immutable', it is the function's telling to the caller "I require immutable (with the literal meaning here)".

Perhaps not an important difference but I think such details are important for teachability.

And there is a difference between parameters and variables because the latter is not a requirement like it is for functions. TO me, an 'immutable' variable carries a meaning for it to be required to be so; 'const' does not have that meaning for a variable. That's why I like 'const' for variables like the following:

  const     a = 42;  // Simple
  immutable b = 42;  // Brings more meaning and supposition

Ali

September 23, 2021
On 9/23/21 2:41 AM, Temtaime wrote:

> Surely, i'm using 'in' anywhere i need const. I'm avoiding using explicit const/immutable.

(You meant for parameters.) Makes sense! :)

Ali
September 24, 2021
On Wed, Sep 22, 2021 at 01:06:59PM -0700, Ali Çehreli via Digitalmars-d wrote:
> Please excuse this thread here, which I know belongs more to the Learn forum but I am interested in the opinions of experts here who do not frequent that forum.
> 
> 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.  I *have* tried this before in various cases, but found that it usually requires a lot of busywork (oops compiler says can't pass const here, can't accept const there, OK let's fix this function, oops, the function it calls also needs const, oops, now it can't accept mutable, must use inout instead, oops, inout is a pain to get right, forget this, I give up), and the benefits, at least so far, are minimal -- it has maybe found 1 or 2 bugs in my code, but only after many hours of refactoring just to make everything const-correct.  So IMHO, not worth the effort.

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.


> Long version:
> 
> It was simpler in C++: We would make everything 'const' as much as possible.  Unfortunately, that doesn't work in D for at least two reasons:
> 
> 1) There is 'immutable' as well

It's very simple. Use const when the data could be either immutable or mutable (usually this applies to function parameters), immutable everywhere else.

But since POD types make copies anyway, in practice I find const/immutable not worth the complexity with PODs. Strings are perhaps the most notable exception to this IME.


> 2) There is no head-const (i.e. no 'const' pointer to mutable data;
> i.e.  "turtles all the way down")

TBH, I see this as an advantage.  However, there *are* certainly cases where you really want head-const, but there's no in-language solution (and Phobos' Rebindable isn't a 100% solution).


> Further complications:
> 
> - Reference semantics versus copy semantics; by type (e.g. slices), by
> the 'ref' keyword, by a member of a struct that has reference
> semanticts (struct is by-copy; but a member may not be), etc.

Yeah, once you get beyond the most trivial data structures, you start running into problems with const/immutable due to the complicated interactions with everything else.


> - 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 it's fallacious to try to put const/immutable on a scale of "better" or "worse".  They serve different purposes: const is to guarantee the recipient of a reference cannot modify the referent; immutable is to guarantee the referent itself can never be modified. The former is like lending your data to somebody that you don't trust -- you yourself can still touch the data but the recipient is only allowed to look at it.  The latter is when the data itself cannot ever change, not even by yourself.


> - Parameters versus local data.
> 
> I am seeking simple guidelines like C++'s "make everything const."

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).  If it's pure POD locally-initialized, just use immutable.


> Let's start with what I like as descriptions about parameters:
> 
> 1) 'const' parameter is "welcoming" because it can work with mutable, 'const', and 'immutable'. It (sometimes) means "I am not going to mutate your data."
>
> 2) 'immutable' parameter is "selective" because it can work only with 'immutable'. It means "I require immutable data."

Const is like a 3rd party contract to ensure that they don't damage your data.  They cannot change it, but someone who has write access (i.e., a mutable reference) still may.

Immutable is like data made of steel: you cannot change it even if you wanted to.  I.e., *nobody* has write access to it.


> But it's not that simple because a 'const' parameter may be a copy of the argument, in which case, it means "I will not mutate *my* data." This is actually weird because we are leaking an implementation detail here: Why would the caller care whether we mutate our paramener or not?

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


> // Silly 'const':
> void foo(const int i) {
>   // ...
> }

This reads less silly:

	void foo(in int i) { ... }

:-)


[...]
> Aside: If 'const' is welcoming, why do we type 'string' for string parameters when we don't actually *require* immutable:
> 
> // Unenecassary 'immutable' but I do this everywhere.
> void printFirstChar(string s) {
>   write(s.front);
> }
> 
> It should have better been const:
> 
> 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.

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 why the compiler does not allow binding string
(immutable(char)[]) to ref const(char)[].

But that also means you don't want to use const(char)[] when ref is involved. Instead, you want string:

	void consumeOneToken(ref string input) {...} // this works

But then, this also means you can't pass in mutable char[]!  So ultimately, one solution is, rebind string to const(char)[] in the *caller*, then pass it to the function:

	void consumeOneToken(ref const(char)[] input) {
		...
	}

	string origInput = "...";
	const(char)[] input = origInput; // ugly ugly
	consumeOneToken(input);	// but at least this works now


> So only the elements of the string should be 'const':
> 
> void printFirstChar(const(char)[] s) {
>   write(s.front);
>   static assert(isInputRange!(typeof(s)));  // Passes.
> }
> 
> (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.


> So, what are your guidelines here?
> 
> More important to me: How do you define your local data that should not be mutated?
> 
>   const     c = SomeStruct();
>   immutable i = SomeStruct();
> 
> In this case, 'const' is not "welcoming" nor 'immutable' is "selective" because these are not parameters; so, the keywords have a different meaning here: With local data, they both mean "do not mutate". Is 'immutable' better here because we may pass that data to an immutable-requiring function?  Perhaps we should learn from string and make it really 'immutable'? But it's not the same because here 'immutable' applies to the whole struct whereas 'string's immutability is only with its elements. There! Not simple! :)

Ugh. I wish people would stop writing const/immutable without
parentheses. ;-)  Always write it with parentheses, and the problem goes
away: you write const(T[]) when you wish the whole object to be const
(resp. immutable), and you write const(T)[] when you want individual
elements to be immutable but the outer structure to be mutable.

But yeah, this only works at the bottom-most level of abstraction. Once
your type becomes more complex, it quickly devolves into a cascade of
exploding const/immutable complexity, and you start running into lovely
conundrums like how to make const(SomeStruct!T) ==
SomeStruct!(const(T)).

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.


[...]
> Personally, I generally ignore 'immutable' (except, implicitly in 'string') both for parameters and local data.
[...]

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


T

-- 
Frank disagreement binds closer than feigned agreement.
September 24, 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

September 28, 2021
On Friday, 24 September 2021 at 17:20:04 UTC, H. S. Teoh wrote:
> 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

I write parsers as splitters: the function takes a string, splits it in two parts and returns them, or the parser is a struct that keeps the string inside.
1 2
Next ›   Last »