Immutability by default

I think this is the kind of change that has the worst benefits on lines to change ratio. But since the process can easily be fully automated (aside from // zig fmt: off parts), I don’t think it matters much.

I don’t think there’s any instance where it solves a problem in a codebase written by seasoned Zig users or even low-level programmers in general. It makes searching for mutable pointer types easier, maybe; I don’t see why one would want to search for pointer-to-const types.

I do think it’s a benefit for programmers that are new to pointers. Mistaking a pointer-to-var for a pointer-to-const is an assumption that can result in runtime bugs, while mistaking a pointer-to-const for a pointer-to-var is a compile error at worst.

If I understand OP’s idea correctly, it’s just a superficial change of ‘*T to *var T’ and ‘*const T to *T’, while the semantics of & don’t change: at a const position, taking the address gives *T, and at a var position, it gives *var T.

3 Likes

“Hand holding and guarantees” → I just want my software to be safe for others.
Zig is amazing at providing that, and I’m wondering if it can become safer still without losing its essence.
“quiche-eating behaviour” → I love quiche so that applies

Your argument can be used as is to protest some beloved Zig safety features:
e.g. non-nullable pointers. I can expand on this if you’d like

3 Likes

No the behavior of the & operator would have the same semantics, i.e. it would produce a pointer-to-const when taking a const declaration or an expression, and a pointer-to-var when taking a var declaration (obviously coercible to a pointer-to-const).

Under the new syntax:

*T == @TypeOf(&const_decl)
*var T == @TypeOf(&var_decl)
2 Likes

Hey, I have added an example of a possible benefit of this change in the original post (EDIT 2)!
I posted it in this thread originally, but then moved it there.
I think guarding against accidental mutation is a benefit to be had with this. What do you think?

1 Like

Honestly, I had that fear when I moved professionally from C++ to Rust: immutability by default felt unnecessarily restrictive. After a couple weeks, it became a new default. To me it makes more sense to choose what you can mutate, rather than what you can’t. So that when you forget to choose, you get something that can’t be mutated on accident.

1 Like

I totally understand your frustration, original post lacks concrete evidence that supports the change. I have edited it with an example :slight_smile:

I would like to point out that this is NOT from the core team. This “proposal” is purely a community discussion that does not reflect the direction Zig is going. So you can jump into Zig and not have to change your core understanding.

Unless one of the Core team make a comment or you see an actual accepted proposal on the Zig repo, this is not a change that is going to happen based on current accepted proposals and compiler discussions. (Caveat: I am not a core team member, so these thoughts do not represent the Zig team at all)

8 Likes

I insists that this wouldn’t be a fundamental change at all, it’s purely syntactical and the transition can be 100% automated. One zig fmt and your entire codebase would be updated, save for the parts where you might be using // zig fmt: off.

Although it would be quite a lot of lines of code, it’s a purely superficial change.

2 Likes

For the code, yes. But there is a mental model shift as well. If one is used to *T meaning a mutable pointer, then muscle memory is to type *T to get a mutable pointer. Reading it as well. You have to change your mental model if you come from C/C++.
The purely syntactical change carries more than just characters. It also comes with a shift in how you write the code, especially if you already have experience in another language without it.

2 Likes

I suppose my understanding of the code is too decoupled from the syntax to fully get the difficulty.

I just have a hard time believing that it’s going to be harder than say arrays not decaying into pointers, comptime control flow, struct-file-namespace conflation, etc. That’s why I don’t think it amounts to much.

2 Likes

Hi, first post on Community.

Making it imutable by default creates a very big constraint for a general porpuse language.
Declaration and attribution are big part of what a programming laguage is.
So creatubg such constraint as “immutable by default” really really needs very good and clear reasonings. Otherwise we would not be going in a general porpuse direction.

This var here to me is just verbosity. I always tend to read this as a constant pointer to the start of the Array (that itself not necessary is immutable, that default don’t make sense to me). Purely anecdotally speaking.
const s1: [] var u8 = ...;

1 Like

I think immutability by default with an additional qualifier for mutability makes more sense, because it is an additional thing you can do with the pointer. Reading can be done via any pointer, but writing is an additional capability that should be made explicit, for example by a function that receives a pointer as parameter and needs to write to it.

The default mutability in C/C++ has historical reasons; when const was added to C++ (and later to ANSI C), it was much more important to not break existing code than it is for pre-1.0 Zig. For the adoption of C++ it was critical that it could compile all the existing C code, so there was no choice.

12 Likes

I think that the example further illustrates the point that I was making of it being a solution looking for a problem without any practical foundation.

Your example:

// poorly named variables, but sometimes you don't have control over that.
// a2 should be const, but I forgot to add the keyword :( because I was distracted e.g. by the smell of freshly baked quiche
fn f(a1: [] u8, a2: [] u8) void {
    // I have now created an unsafe/less-safe zone where
    // I can mix them up on accident and modify a2
}

Even if we grant this dubious hypothetical where you can’t rename the arguments of the function that you are writing for “reasons”:

  1. Currently you forgot the const, under your proposal you forgot the var. The code is bugged and/or poorly written either way and needs fixed, we are merely bike-shedding over which argument should get which keyword.
  2. Regardless of the correctness of the annotations, the arguments are being used incorrectly. This is the actual problem, not the mutability. If nothing else, you are making an argument regarding poorly named variables, which has nothing to do with mutability.

My point it, this is not a technical issue, nor a safety one: it is a style preference. It is a very reasonable style to prefer, many people would like it over the mutability-by-default, but it is quite the stretch that we are getting an benefit here that warrants breaking the status-quo and requiring everyone who uses the language change their entire mental model of it.

1 Like

I think there really is a difference. If you use the OP’s syntax, you’ll notice it when you forget to write var while implementing the function. But with the current syntax, if you forget const, you might not notice the mistake until the caller passes a const reference parameter that should have been appropriate. If the caller and the function implementer aren’t the same person, the caller is likely to be confused, unsure whether they called it wrong or if the function signature itself was designed incorrectly.

6 Likes

I think whether we write []u8 and []const u8 as in status quo or []var u8 and []u8 as proposed doesn’t really matter. Neither isn’t catching any real issues (in fact both are kind of flawed, since the memory of a const pointer can point to a var). I personally prefer the first one, as it’s what I’m used to.

I would also like to point out (local) variables/constants, where Zig has in my opinion chosen the perfect strategy: There is no default, both var and const are options, but, and that’s the important part, const is enforced where possible. I like this because it sidesteps the argument, while ensuring more safety than a pure change in naming scheme could ever achieve.

I think if you struggle with missed const then a linter would be a better solution than forcing the entire language to change over a minor naming difference.

4 Likes

What about we just start with allowing the var syntax, so absolute zero change to current code required but people can write []var u8 instead of []u8 if they want. So the default stays the same but we can simple write []var u8 and []const u8 if we want to. Later we do a second proposal to switch the default after people get used to reading []var u8 It marks all the zig checkboxes to be explicit, full backwards compatible and both parties can write exactly the style they prefer.

3 Likes

You are missing the point that the error isn’t the mutability, nor is it “fixed” if the status-quo of mutability were to be changed.

Granting the entire unlikely hypothetical where we can’t rename our own variables to be meaningful, compounded with our mistake of not adding const to a slice where we should have, and then furthermore getting so confused by this that we start trying to mutate the wrong slice due to our previous mistakes and not understanding our own code, then yes, we might benefit from a slightly more meaningful error at the call-site instead of later where the broken logic could be less obvious.

Is this a common enough scenario that the language needs a foundational change for? I am open to my mind being changed for real-world problems being solved, but this isn’t compelling enough for me yet.

I’m always in favor of more const. Over time, it allows the compiler to do more things.

More const means that more functions are likely to be comptime-able without change. That’s probably a benefit.

More const, however, is likely to confuse people more. The fact that there is a different between i32 and comptime int and f32 and comptime float already causes some confusion.

I don’t think that’s fair.

Under the current syntax, if you mix them up (meant a2: []u8 not to modify its pointee, be []const u8) you might get a buggy program that might compile.

Under the alternative syntax, if you mix them up (meant a2: []u8 to modify its pointee, be []var u8) you will get a program that doesn’t compile and points out your mistake.

Compile errors are better than runtime bugs.

4 Likes