hsutter / cppfront

A personal experimental C++ Syntax 2 -> Syntax 1 compiler
Other
5.27k stars 225 forks source link

Const by default #815

Open MatthieuMv opened 8 months ago

MatthieuMv commented 8 months ago

I believe that knowing which variable is subject to change easily reduce the mental overhead of code debug/reviewing/maintenance. The issue with C++ is that you can declare a variable that should not change by forgetting the 'const' keyword. C++ compilers throw an error when modifying a const variable but they can't tell you when you are modifying a variable that you shouldn't because of algorithm logic.

Adding 'const' to any variable and function argument is a practice that my team and I have been practicing over 3 years. The obvious issue with this practice is the increased mental overhead of writing code, as you have to remember it when writing a variable.

So, what do you think about initializing variable and function arguments with const by default ? We can use a 'mutable' or 'mut' keyword to tell that the variable is subject to change when it is really required.

JohelEGP commented 8 months ago

and function arguments with const by default

This is already the case for function arguments, which default to pass-by in.

JohelEGP commented 8 months ago

From https://github.com/hsutter/cppfront/issues/153#issuecomment-1336165146:

See https://github.com/hsutter/cppfront/wiki/Design-note:-const-objects-by-default, and https://github.com/hsutter/cppfront/issues/25#issuecomment-1264512921.

JohelEGP commented 8 months ago

Statements can also have parameters (https://github.com/hsutter/cppfront/wiki/Design-note%3A-Defaults-are-one-way-to-say-the-same-thing#from-named-functions-to-lambdas-to-parameterized-blocksstatements-to-ordinary-blocksstatements, https://github.com/hsutter/cppfront/issues/386#issuecomment-1518194517). So if your const variable would be scoped to a statement, you can use that to declare it:

(i := …) // `i` will be an `in` parameter, thus `const`.
  if (…) {
    …
  }
ntrel commented 7 months ago

Declaring a runtime constant is quite noisy:

v := expr;
c: const _ = expr;

That's enough typing and noise to avoid marking constants. So the status quo encourages bad practice: https://www.reddit.com/r/cpp2/comments/16ftsw7/suggestion_local_objects_const_by_default/

From https://github.com/hsutter/cppfront/wiki/Design-note:-const-objects-by-default:

the majority of local variables are, well, variables

I believe this statement is false in modern code. The author of the Vale language wrote:

To our great surprise, we've found that our codebases have a lot more declarations than assignments

We sampled three Vale projects. One had 111 declarations, and only 35 assignments. That's only 21% assignments! The other two were even lower, at 20% and 6%.

This isn't just Vale either. A randomly chosen Rust library, Rocket, had about 8%.

https://verdagon.dev/blog/on-removing-let-let-mut#why-we-like-it

SebastianTroy commented 7 months ago

Instead of switching to the default and requiring mut, or similar, perhaps :== could imply const?

On 24 November 2023 14:44:36 Nick Treleaven @.***> wrote:

Declaring a runtime constant is quite noisy:

v := expr; c: const _ = expr;

That's enough typing and noise to avoid marking constants. So the status quo encourages bad practice: https://www.reddit.com/r/cpp2/comments/16ftsw7/suggestion_local_objects_const_by_default/

From https://github.com/hsutter/cppfront/wiki/Design-note:-const-objects-by-default:

the majority of local variables are, well, variables

I believe this statement is false in modern code. The author of the Vale language wrote:

To our great surprise, we've found that our codebases have a lot more declarations than assignments

We sampled three Vale projects. One had 111 declarations, and only 35 assignments. That's only 21% assignments! The other two were even lower, at 20% and 6%.

This isn't just Vale either. A randomly chosen Rust library, Rockethttps://github.com/SergioBenitez/Rocket, had about 8%.

https://verdagon.dev/blog/on-removing-let-let-mut#why-we-like-it

β€” Reply to this email directly, view it on GitHubhttps://github.com/hsutter/cppfront/issues/815#issuecomment-1825771040, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AALUZQNPPJRWWZMMX6JJL53YGCXFBAVCNFSM6AAAAAA7IB4US2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQMRVG43TCMBUGA. You are receiving this because you are subscribed to this thread.Message ID: @.***>

hsutter commented 7 months ago

We sampled three Vale projects. One had 111 declarations, and only 35 assignments. That's only 21% assignments! The other two were even lower, at 20% and 6%.

Thanks, data is great and I appreciate it. Followup questions please:

1) Do you know why they only counted assignments? Assignments are only one of the mutating operations.

2) The three example reasons they give for fewer assignment seem to follow the pattern that writing in a more functional programming style (if-conditions, calling algorithms instead of loops) leads to less mutation, which is of course true -- the endpoint of that is the pure functional languages that have no mutation at all, only immutable state. I wonder how representative it is though of code in the major systems languages. In general, functional programming styles have not succeeded in getting broad adoption in the mainstream.

3) Having only ~100 declarations seems like a very small sample.

Do you know of data on larger projects, ideally C/C++/C#/Java code, and counting the other non-const operations in addition to assignment?

AbhinavK00 commented 7 months ago

I don't have the data but here's an article by creator of Odin language which talks about various kinds of syntax for declarations.

cpp2 seems to fall in the group of "name-focused" declarations and I can't help but notice that the two languages (Odin and Jai) which have name-focused declarations have same syntax for function, type and constant declarations. That is expected as functions and types are kind of "constant" bindings, you can't assign a new function to same name (not true for anonymous function but well, they are anonymous). Even Zig declares struct like the following

const A = struct {
    x: f32,
    y: f32,
};

cpp2 has an inconsistency in this case.

JohelEGP commented 7 months ago

type implies const in t: @struct type = { }. Much like Cpp2 v :== 0; and Cpp1 constexpr int v = 0; imply const for v.

AbhinavK00 commented 7 months ago

That's not very obvious (I didn't even think about it like that before). I also don't know how that extends to functions and the constexpr examples doesn't really make sense to me since there are no mutable constant expressions and one can argue that it's a cpp1 thing too.

JohelEGP commented 7 months ago

I also don't know how that extends to functions

Named functions are also constant.

AbhinavK00 commented 7 months ago

Named functions are also constant, type also implies constant, why can't variables be constant too?

JohelEGP commented 7 months ago

They can be constant. It's just not implied by default. You need to explicitly declare them const.

hsutter commented 7 months ago

I tried an experiment where every local variable is emitted as const by default, just to see what would happen in the cppfront code and test cases. Here are some initial observations, in no particular order.

Of 98 test cases, 41 succeeded and 57 failed. It seemed that the 41 were mostly the smaller/simpler ones that were exercising the compiler (e.g., testing grammar or lowering) rather than trying to do a sample computation.

In some cases, making a local variable const by default caused copies instead of moves to happen. This happened for return values and for forwarding parameters.

Some types' natural usage is non-const, including when they appear as local variables:

Other patterns I noticed:

I stopped partway through... the results weren't encouraging enough to continue the experiment at this time, but I'm open to taking it up again in the future. For now, my results seem to confirm the hypothesis that 'local variables tend to be, well, variables.'

hsutter commented 7 months ago

Named functions are also constant, type also implies constant,

These are inherently static (compile-time) and ODR-necessary in a statically typed language.

Unlike objects, which are inherently run-time dynamic values (and sometimes dynamic types, see previous).

why can't variables be constant too?

(I have to poke a gentle smile at "variables" in the quote here -- I know you mean "objects," but the phrasing "variables be constant" is a little self-answering so I'll point it out just a little bit. 😁 )

Many Cpp2 objects are const by default, including parameters. The ones that are not are function-scope and type-scope objects.

I do think it's interesting that the discussion tends to focus on function scope objects, but not type scope objects. So I have a suggestion... instead of focusing only on function local objects, please also consider type scope objects:

Why (or why not) should type scope objects be const by default, based on their typical usage? And why (or why not) should the default be different for function scope objects, in principle?

AbhinavK00 commented 7 months ago

You've very much convinced me. cpp is a language that doesn't lend itself to have const by default.

Also, const member data doesn't make much sense, they come with problems like making the class's move constructor slower, no std::swap etc. And the better way is to have private members with no public API to change it.

But, can cpp2 still have some terser way to declare const variables?

JohelEGP commented 7 months ago

I do think it's interesting that the discussion tends to focus on function scope objects, but not type scope objects. So I have a suggestion... instead of focusing only on function local objects, please also consider type scope objects:

Why (or why not) should type scope objects be const by default, based on their typical usage? And why (or why not) should the default be different for function scope objects, in principle?

These are the related guidelines that are still relevant in Cpp2:

Type scope objects shouldn't be const by default because most types should be regular. A function should get to decide whether its objects are const if possible. The default should be the same for objects in functions, and everywhere, for consistency.

When commit 3512ecd41f33f7e812a070e3370fd97845dd7d24 came around, I had to change 11 :==s to :=s, for a total of 23 :=s, and managed to keep 5 as :==. Before that, I had 9 :=s (which excludes : 𝘡𝘺𝘱𝘦 =s). I could make the copies, which :== didn't necessarily do before. That's in my GUI code, where most local variables are const. What I mostly need to modify there are parameters of polymorphic types (possibly the this object).

Now I'm generalizing @array (https://github.com/hsutter/cppfront/discussions/797#discussioncomment-7622822). In writing the logic, I find myself having mostly local variables that I need to populate. But the pipelines you write with it, and the tests I write for it, are the other way around. In fact, good pipelines don't even have local variables: https://github.com/hsutter/cppfront/issues/741#issuecomment-1754194152.

It seems like how many non-const local variables you have can be heavily dependent on the paradigms used locally.