-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
syntax: drop the const
keyword in global scopes
#5076
Comments
If I understand, global non-constants will still be Also I think this is good, since the inconsistency of global vs. local scope syntax is worth the amount of unnecessary times you currently type Would a global |
The old way would turn into a parsing error. However |
global scope is the same as MyType = struct {
Self = @This();
buffer_size = 0x1000;
buffer: [buffer_size]u8,
}; a little harder to tell apart constant declarations from field declarations without the bright keyword still probably a good change though. |
Great point, I hadn't considered that. I can't remember if there is already a proposal open for this or not, but it could be required that all field declarations are before the first global variable declaration. That would help a lot. |
Assuming the programmer can still specify a type for a const global, there's an even more ambiguous case:
The compiler could still check if the declaration ends with |
Argh. OK that's a problem to solve. |
How about a special section in struct definitions for where members are declared? seperate from where associated values are stored, so that they are syntactically distinct |
The nuclear option is to remove Maybe it would be reasonable to further diverge const and var global syntax by removing pub Foo = struct {
a: u32 = 6,
b = @as(u32, 4);
}; Now it's pretty clear. Acknowledging @theInkSquid's suggestion, this would work: pub Foo = struct {
a: u32 = 6,
---
b: u32 = 4;
}; But I don't like introducing that |
Another option would be to put a new keyword on fields, but that's kind of just trading where the typing happens and introducing the new difference between struct and function scopes. That said, there are probably a lot more imports and static declarations than field declarations in most code. pub Foo = struct {
.a: u32 = 6,
.b: u64,
apply = fn (self: *@This()) u64 {
return self.a + self.b;
};
}; |
@SpexGuy you've had a lot of insightful comments about syntax recently. What's your take on the issue, what would your personal preference be? |
@SpexGuy I especially like that, since it mirrors struct initialization syntax. My only concern is how a |
Thanks! |
Thanks, good to know! I'll leave this issue open for a day or two so that people have time to sleep on it, and then I'll resolve it one way or the other, so that #1717 can get unblocked. I'm pretty set on doing the |
For clarity, we'd be removing |
@Tetralux Yes, in fact, files are structs, so there is not even a difference between those two. |
I know there are folk who aren't fans of
std := @import("std.zig");
math := std.math;
assert := std.debug.assert;
mem := std.mem;
run := fn () void {}
pub Foo := struct {
// fields immediately visible as per dot
.a: u32;
.b: u32 = 5;
.c := true;
// globals
a: u32; // compile error
a: u32 = undefined;
b: u32 = 5;
c := true;
var d: u32 = 6;
var e := false;
} |
I’m a weak no. I would much prefer declarations and reassignment to look sufficiently different. This helps keep the code scannable and greppable. What about stealing Odin’s |
I think this has come up before somewhere, but what about making struct and functions look similar:
I am not even sure I like it, but it does have a certain amount of symmetry to it. Pure bikeshedding of course. It isn't clear how the "files are structs" thing would work with that. Personally I think the reliance on seeing the difference between a comma and a semicolon is far too small for comfort. With some fonts and color schemes, this is really not obvious to me. Perhaps my eyes are too old. One thing about the previous proposals with the removal of types is with function parameters. Those still have the form |
extracted into separate issue: #5077 |
I think the proposals with |
Indeed that is a complicating issue. Here is my attempt; I thought it would look too odd, but turned out better than expected: #498 (comment) |
I realized that this proposal has one really big downside: it makes it no longer possible to cut+paste constant declarations from global to local scope, and vice versa. I think that's actually a really big deal. However I'm also not willing to make function definition syntax as cumbersome as: const foo = fn() void {}; Stuck between a rock and a hard place. So what happens? Something gets crushed. I am now proposing to also drop const in local scopes for constant declarations. But what about the problem I noted in the original post above? I have a solution, which is to make test "reassignment" {
var x = 10; // var syntax is unchanged
mut x = 20; // reassigns x
x = 20; // compile error: x is already declared.
} With this new proposal, there is a new keyword, Demonstrating how this interacts with #498: test "example1" {
var b: i32 = undefined;
a, mut b = .{ 1, 2 };
}
test "example2" {
var b: i32 = undefined;
a, mut b, var c: i32, d = .{1, 2, 3, 4};
} This strongly encourages SSA form and const-by-default, removes a lot of syntactic noise, and keeps global / local variable declarations copy+pasteable. This is pretty big change, however, once again it is the kind of change that The other kinds of assignment that do not look like a += b; // unchanged
a.b = c; // unchanged
a.* = b; // unchanged
a[i] = b; // unchanged Variable declarations are unchanged. As a little test, I updated the process_headers.zig tool to this proposal: https://gist.github.com/andrewrk/55ca383d615e34a537a589f2ac100aa7 There were actually zero instances of |
@andrewrk I think that saying // unify assignment with other operators, like +=, *=, etc.
a #= b;
a .= b;
// looks like "a gets the value of b", but is inconsistent with other operators
a <- b; with the My preference is |
Here's another alternative which should address the destructuring scenario. Always suffix the name of the variable being declared with a colon. At a glance it might look like @mikdusan's suggestion but is a bit different. a: = 12;
b: i32 = 12;
var x: i32 = undefined;
y:, x, var z: = .{ 1, 2, 3 }; // const decl, reassignment, var decl
f: = fn() void {}; Interestingly, it ends up looking like a variable declaration with explicit type, but the type is "invisible". Edit: As pointed out, this is actually basically what @mikdusan proposed in #498 (comment) |
It seems like there is a major impasse based on these reasons:
I am extremely in favor of dropping
x := y; // const declaration
var x := y; // mutable declaration
x = y; // reassignment
//multi assign
a +=, b :=, var c :=, d = someFn();
// ^ ^ ^ ^
// | | | |
// | | | +- reassignment
// | | +----------- mutable declaration
// | +----------------- const declaration
// +----------------------- modification While this does look a bit weird at first, I expect the VAST majority of functions to return just one or two parameters. a :=, b := foo(); // can clearly see 2 definitions
a =, b = bar(); // clearly 2 reassignments etc. However, it isn't any less clear than saying This proposal is a much smaller change than some of the other ones involving |
fn reassign(lhs : Identifier, rhs: Expression) void { // compiler magic
fn declare(isReadWrite; bool, lhsName: String, rhs: Expression) void { // compiler magic Those two functions seem like they do different things to me, so the binary operators representing those functions should be different as well.
As for destructuring syntax, I would let the operator be king, and force the operands to comply. Just use multiple lines to handle the special cases. // case 1
var x, y, var z = getCoordinates(); // with '=', x,y,z must not be declared earlier
// case 2
x,y,z .= getCoordinates(); // x,y,z must be declared earlier and all be 'var'
// case 3. Just use multiple lines if you have to mix reassignment and declarations with destructured <decl/reassign>.
var x : f64 = 0; // x is needed also after the while loop
while(cond) {
var y : f64 = undefined;
var z : f64 = undefined;
x, y, z .= getCoordinates();
cond .= update();
}
// case 4
// following this principle, the destructuring syntax would work for ALL operators that do reassignment
x,y,z += speed();
dx,dy,dz -= acceleration(); Edit. Took the time to try to check how the new syntax looks on some of my code (sudoku solver). I based the changes on this post. My conclusion is that it looks quite OK to me, except that |
@user00e00 I am in favor of your proposal 90%. Declarations should be
|
Sadly, none of the vacant derivatives of the I also thought about the lack of control over reassigning/declaration, but in other cases in zig you are encouraged to just use more lines of code. One example is if you want to mutate something that was passed in as a function parameter. You have to "copy" the value into a mutable variable then.
I would be okay with Anyway, I would rather have declarations being |
I think both of these suggestions come from the same place. A while back, I noted:
These proposals sidestep that problem in what I think are the only two ways: @theInkSquid puts the entire operation on each variable and removes the shared part, and @user00e00 puts the entire operation on the shared part and removes the per-variable part (but keeps const-ness per-variable). Both proposals then take the natural step of extending to allow more operators than just assignment. The First with regards to where operators go, there are a lot of pros and cons to consider.
Isn't that beautiful! Note that this is not analagous operator overloading. There is no hidden behavior here, everything is well-defined and nothing unexpected can happen. So that leaves us with a complicated but explicit option and a simple option that might encourage bad practice. In terms of I think keeping const and function statements is also still a valid choice to consider. It's much easier to search for in a large codebase than |
After thinking about it somewhat, I have to agree with @user00e00 's proposal more and more. Mixing a declaration and reassignment on the same line is abolsutely a bad thing to have in the language. I also agree with @SpexGuy that allowing So in all: a, b, c = foo(); // assign to all
a, var b, c := boo(); // declare all I think for specifying types, pub Foo := struct {
.x i32 := some_default; // since this is a declaration, use := for default value
.y f64;
init := fn(a i32) Foo {
x i32, var y := bar(a);
// a is explicitly typed, y is inferred
y += 10.0;
return .{ .x = x, .y = y }; // fields not declared here, use '='
};
}; I guess the question here is: should struct literals use Edit: the question of reassignments has come up. I believe this would be a fine idiom: var x := something;
...
// to reassign to x and to declare a new variable with multi assign:
tx, y := otherThing();
x = tx; |
To me
|
TL;DR: replacing I know much has been discussed already, but I had the urge to write down my take on this. I'll quote @kyle-github 's response line by line and add my comments:
This requires
I think the example below makes function definitions concise and, most importantly, readable. Moreover, it is very consistent among def Foo = struct {
a: i32, // field
b: Bar, // field
def C = 50 // const
};
def Bar = enum {
def Self = @This,
A,
B,
C
def modify = fn(self: *var Self) { // note explicit default mutability
self.* = .A;
}
};
def main = fn() void {
def bar: Bar = .A;
var foo: Foo = undefined;
foo.a = 3; // fine
bar = .B; // error
var mfoo: Foo = .B;
mfoo.modify(); // now, mfoo == .A
baz(&foo);
def str: []u8 = "hello"; // note default immutability
}
def baz = fn(foo: *var Foo) void { // note explicit mutability
foo.b = .A;
}
I see this as a minor advantage that seems to be bringing many complications into language design. Honestly destructuring would not convince from moving into Zig as I cannot see it as a convenient feature (probably because of my background in C), and I would like to think there must be other ways that do not require new sigils. And if there are no ways to achieve this, I would rather drop the feature than complicating the whole design because of it.
Totally possible with the syntax from the example above, as it was already with status quo.
Not happening anymore with the syntax described above.
The syntax above seems what's most readable and consistent to me in Zig. Again, keywords are more readable for humans than operators. Just note how GNU Make is all about operators and how cryptic it becomes.
The syntax above defines one way to do things, as does status quo. The only thing to do is replacing
Totally agree. Keyboard issues aside, operators need an extra cognitive load as we humans (unless you are a total nerd 😄 ) are more used to read words than mathematical symbols
Totally agree. Removing the
And finally:
Solved with default immutability once
As said by others, Edit: added my two cents on the |
Just now, I was inspired by // The var keyword always defines new variables
var { x_1 , y_1 , z_1 } = getCoordinates();
// Variables defined above, re-assigned here
// The behavior is exactly the same as std::tie, the existing variables are used here
let { x_1 , y_1 , z_1 } = getCoordinates();
// If we can do this
let {
var x_2, // new variables defined
y_1, // variables defined above, re-assigned here
z_1
} = getCoordinates(); Is it similar to pattern matching? |
I guess I have a question: why is it important to allow both constant and mutable declarations in a multi-result destructuring statement? Is it really that important? Along the lines of what @nyu1996 said, I was thinking of other languages where destructuring was done with parentheses:
Each of Since this is all pure bikeshedding at the moment, I have to say that I am not all that thrilled with the reuse of Personally I like Is mutability a property of a variable or a type? Which of these should be allowed, excuse the handwaved syntax:
|
Agree. var (a, b, c) = some_func(foo); // mutable
(d, e, f) = some_func(foo); // const |
This proposal has two parts:
Which makes it the same as Option 5 but with a different word.
Mutability in Zig is a property of a memory region. So for any value type, the mutability of its backing storage must be specified. For a pointer, the mutability of the memory it points to is part of its type, but the mutability of the top-level pointer value is part of the variable. The proposals here that use
I don't think anyone wants to do both at the same time with the same keyword, so there's no ambiguity.
I don't want to get lost in the weeds of destructuring syntax. That conversation should happen in a different issue. Whether we do I feel like we've fallen into the syntax pit again, so here's my attempt to bring us out of the bikeshedding and into the problem. The first question we need to answer is whether a single destructure list needs to be able to contain both overwrites and declarations. Is there a concrete use case where we need this ability?
If we don't need both overwrites and declarations in the same statement, then the keyword doesn't need to be separable from the
I don't see any way to make the language unambiguous without selecting one of these four options. (Edit: fix links) |
I think keeping the status quo for Nice things about: const x = …;
var y = …;
Another thing of the current way: It's trivial to change the mutability of a declaration without affecting any code: Replace Yet another thing for me personally is that i search for declarations in zig by using And yet another thing is tooling: I can find all declarations in a project by just using grep -rniIE '(var|const) [A-Za-z0-9_]+ =' Which is really useful (also, zig std has 18766 declarations) and would be much harder with any of the proposed versions as we suddenly would have two syntaxes two declare something Damn, this is way more text than i intended to write... |
@MasterQ32 , please my comments on #5076 (comment) which explain why |
@XaviDCR92 I don't think // Type declarations
const ImmutablePointer = *u32;
const MutablePointer = *mut u32; This also follows Communicate intent precisely as soon as you see your first |
@MasterQ32 please read my proposal #5076 (comment) again thoroughly and also read #5056 , where I already suggested a syntax for mutable pointers. OTOH, having already |
Going back to the original motivation: fn foo() void {} // before #1717 -> optimally concise
const foo = fn() void{} // after #1717, "brutal"ly verbose
The current syntax for declaring a const: const foo : T = val; If function decl is to be consistent, wouldn't it have const foo : T = val
const foo : fn() void = {} Doesn't the original function decl syntax derive its const voo T finalval;
const foo fn() void {}
const foo () void {} // is "fn" really required for fn types?
var voo T = initval;
var foo fn() void = {} How to distinguish struct members from struct globals: const pub Foo struct {
// 5 fields (occupying space in each instantiation of this struct)
var a u32 = 6;
const b u64 7_777_777_777; // a const field (why not)
var cb1 fn() void = null; // a var function pointer called cb1
var cb2 fn() void = {gr();}; // a var function pointer called cb2
const cb3() void {gr();} // a const function pointer called cb3
// for globals (namespace use of Foo)
static const bufsize 16716; // a const global
static const g1 (-bufsize); // a const global
static var g2 u32 = 89999; // a var global
static const helper1() void { ... } // a const global helper function
static var helper2(b anytype) f64 = { ... } // a var global helper function pointer
} Destructuring assignment and multiple return values, |
Are not all functions constant? And are not all structures constant? Would it be reasonable to eliminate some of the boilerplate, and step back from pub struct Foo = {
// some fields
}
struct Bar = {
// some other data fields
}
pub fn main() void = {
// some things to do
}
fn bar(const a i64) i64 = {
return a * a;
} People coming from other languages know that data structures and functions are constant. So from a consistency standpoint, having |
That is my motivation behind replacing Moreover, the syntax I described on #5076 (comment) makes the following construct more readable, which allows reducing repeated code when defining many functions that share the same signature (typically used for callbaks): def Foo = struct {
self: @This,
def CmdHandler = fn(self: *var Self) void // Signature for callbacks
def cmd_a = CmdHandler {
// Function body for cmd A
}
def cmd_a = CmdHandler {
// Function body for cmd B
}
}; C programmers would have to use macros to provide similar functionality: #define CMD_HANDLER(f) void f(Foo *foo)
CMD_HANDLER(a)
{
/* Body for cmd A */
}
CMD_HANDLER(b)
{
/* Body for cmd A */
} |
I do appreciate the ability to declare a callback signature as a definition to be re-used. |
Answering some questions:
This is discussed in 1717 (comment), but the TL;DR is that parameter names aren't part of the type (and shouldn't be), so this doesn't work. Some alternatives were proposed, e.g.
but ultimately not accepted for this specific change. They also weren't rejected though, so could be valid as follow-up proposals.
That's part of it, but there's also the
By zig's definition of
Again, I don't really know what this would do besides take up space. Since the compiler knows it's constant and changing it is UB, it's going to inline it into a
Yes, but so are integer literals. That doesn't make At comptime, this code is valid:
Both of these struct literals are constant, but the type 'X' is not. With structs this only works at comptime, but with functions runtime With the semantics from 1717, the new code
This is definitely a weird use case but I guess it could be used for runtime function patching or for JIT:
edit: fix patching example syntax |
Wew, excuse me for not reading everything completely. I am opposed to the idea of having no keyword at all for definitions. It makes it hard to see whether it's a reassignment or a definition and it will be very confusing. I already have this problem reading Python code. Is it a new thing or a reassignment? Also opens you up for having bugs of the kind where you wanted to reassign but made a typo and now you suddenly have a new constant. And to be honest I'm not sure I see why Reading a bit more, I think I mostly agree with @XaviDCR92 in #5076 (comment). Removing |
I think the issue with I think whatever syntax is decided, it should be smaller and less intrusive, making the content that's stored be more immediately readable. |
I would like to follow up on my previous comment: this is why I think here are some examples: std := @import("std");
pub MyStruct := struct {
.a: i32;
.b: i32 := 10; // use := here because this is declaration
pub init := fn(x: i32) MyStruct {
return .{
.a = x, // use = because it was declared earlier
.b = x,
};
};
}; Extending this, mutable variables could be declared like this: var x := z;
var y: usize := 10; |
Thank you everyone for the high quality discourse here. It's amazing to me that I can propose a change like this, and then very quickly get a well-informed, detailed picture of the design landscape based on people's feedback and ideas. Special thanks to @SpexGuy for keeping the topic clear and focused. This is a particularly difficult design decision, and it's inevitable that any decision would leave some people disappointed. Such is the nature of these things. Anyway, I've made a decision here, which is to accept the null hypothesis of status quo. #1717 is still planned. I'll make a follow-up comment on that proposal. I still think it will be annoying to type such long function declarations, but such is the nature of Zig's design. We pay the cost of boilerplate and more keyboard strokes, and gain simplicity and consistency. |
And now please enjoy this video made by @theInkSquid https://www.youtube.com/watch?v=880uR25pP5U 🤣 |
This proposal is to change global const syntax from
const a = b;
to simplya = b;
. Combining this proposal with #1717, here is Hello World:For functions that are not public, this proposal softens the extra syntax bloat that #1717 would introduce.
Global variable syntax remains unchanged. Local variables and constants remain unchanged.
Anticipating a comment on this proposal:
The problem with that is demonstrated here:
It's OK for the syntax to be non-symmetrical between global scope and local scope, because there are already different rules about how these scopes work. For example, globally scoped variables are order-independent, while locally scoped variables are evaluated in order.
This proposal improves another common use case as well which is defining structs:
Structs are still always anonymous, but now it looks more natural to declare them.
This is the kind of change where
zig fmt
can automatically upgrade code, so it's very low burden on zig programmers.The text was updated successfully, but these errors were encountered: