Hello, this is my very first post. I’ve been a casual reader, and the community seems welcoming.
(sorry for moving it around, I guess it should be on Explain, i’ll leave it here, if I’m wrong somebody please move to the correct location)
I’m learning Zig on my spare time… One thing that’s been confusing is the overuse of the dot.
Field/member access: engine.health
Explicit pointer dereferencing: engine.*.health
Pointer-to-struct field access: engine.health (instead of engine.*.health, or a different notation engine->health)
Struct field initializer: DroneEngine{ .health=100 };
What I’m saying could be wrong, if so I appreciate tips on how to best use it. But in general seems like “.” means too many different things and could be replaced by another notation in some cases?
It sounds like you actually are not confused, since everything you wrote is correct (although Zig calls what you have written as “anonymous enum” instead as something more like “anonymous union”). Is there a place where the dot has tripped you up in your learning?
One of the nice things about Zig syntax is that it is designed with being tokenized and parsed quickly in mind. For example, in each of the cases that you mention, with the exception of field member access through and not-through a pointer, the dot can only mean one thing, and leaving it off would imply a different (often incorrect) syntactical structure. In that exceptional case, only one is possible depending on the type of whatever came before the dot.
Is there a place where the dot has tripped you up in your learning?
Yes, I’d say the first place was on things like
var status: EngineStatus = .off;
As in “where is .off coming from” ? This kind of omission seems quite common usage in the language (starting to get used to it though while learning explicit is clearer).
The other was:
DroneEngine { .health = 100 };
Which I consider the “.” here superfluous (imo, lhs, it’s a field, why the punctuation requirement?)
And I’d expect this one to error
print("{}\n", .{engine.health});
if engine is a pointer, it should be dereferenced (engine.*.health)… right ?
How come engine.*.health and engine.health can both be accepted ?
That syntax can be super confusing at first (though i admit it makes me really happy now). The .off is a member of the EngineStatus struct. You are able to drop the struct because the Result Type is known. I.e we know that status has to be an EngineStatus type. So .off is just syntax sugar for EngineStatus.off, where .off is a means of initializing an EngineStatus. It’s clean way to provide multiple default ‘constructors’ as it were.
DroneEngine { .health = 100 };
I believe this syntax is borrowed from C’s designed initializer syntax. If not borrowed, it looks a lot like it
For this one, engine.*.health and engine.health is the same thing.
However, engine.* and engine are not. The input of the print function is immutable, so both of them are valid. If you deref it, the Zig will automatically choose to copy or refer it depending on the speed. Zig will choose the faster one (and possible one)
If you don’t deref it, or if your data in the engine is not possible to be simply copied, it will refer it.
However, technically, everything in Zig is copy, if you use the pointer and don’t deref it, Zig will still copy the address.
And if you make a function with mutable inputs, they have to be pointer.
what did you discover to explain these occurrences of the dot?
I guess I discovered that learning what the dot does depends a lot on the programmer to interpret the environment it’s being used. That’s why when learning the language can feel a bit confusing (though is not 1.0 release yet, so maybe it’s on me). But instead of using dot for many different operations, using a different symbol to express a specific operation could be useful ? (Just an opinion)
And even though I don’t use LLM to write code, it’s being one of the few resources I can reason with for learning Zig, like “explain me this” , “why doesn’t it work like that”, “how to understand what this function wants”… ? It gives me wrong answers all the time too, but helps pointing in a direction.
Maybe in the future std can include copy-paste examples for every function.
I believe this syntax is borrowed from C’s designed initializer syntax. If not borrowed, it looks a lot like it
Yes, I had a similar thought, which is also why I was expecting -> for pointer member access. Instead of being just able to solve .
At first glance these dots seem unnecessarily verbose
(and the .off omission can be a stretch, if you are just learning)
Can anything else be there that would require the dot notation to resolve the field ? Or it’s just really borrowed syntax to help transition from expected C developers ?
If you don’t deref it, or if your data in the engine is not possible to be simply copied, it will refer it.
Hi, are you saying that .*.health and .health behave differently in the language ? If so can you demonstrate with a piece of code I can try as well ? Otherwise, if both provide the same end behavior, is the .* notation needed ?
And if you make a function with mutable inputs, they have to be pointer.
No, in this specific case, the compiler emits the same machine codes. I guess (I am not sure sorry) the author of Zig just try to represent the engine->health and (*engine).health in C, so one is convenient, the other is more explicit. Andrew says he doesn’t want to remove the C’s power from Zig.
However, it will be more interesting if you also bring the actual struct into this discussion:
pub const Engine = struct { health: i64 = 100 };
var engine: Engine = .{};
const engine_ptr: *const Engine = &engine;
engine.health = 80;
std.debug.print("Print health from engine: {}\n", .{engine.health});
std.debug.print("Print health from engine pointer: {}\n", .{engine_ptr.health});
These two prints will have the same result: 80. But in the first one, the Zig will decide if the engine need to be deref or just copy the whole thing, which one is faster. In the second one, Zig will deref as you wish. I guess, again, I guess, from a user’s view point, the dot really make no different in these two prints, because it can be just mean that “I just want to read the field from an object, deref or not, not my business.” C does not have this benefit, Zig has, because Zig can take the benefit from the immutable pointer access (*const in this case). So in C, it’d better have a different symbol to access a field from a pointer, but in Zig, it may not need a different symbol, because Zig can do immutable deref.
It becomes more important if you want to pass the whole struct to a function, which then modifies it. Then another function will read it.
pub const Engine = struct {
health: i64,
};
// This will not modify the original struct.
pub fn addOneHealthWillNotModify(engine: Engine) {
engine.health += 1;
}
// This will modify the original struct
pub fn addOneHealth(engine: *Engine) {
engine.health += 1;
}
// Zig will copy the fields if copying the values possible and
// faster. It will copy the address of the fields if copying values is
// not possible or slower.
pub fn getHealth(engine: Engine) {
return engine.health;
}
// So generally this is not very neccessary in Zig.
pub fn getHealthFromPointer(engine: *const Engine) i64 {
return engine.health;
}
Of course, a more modern and more Zig way to do it will be:
Zig tries to make every line unambiguously readable without knowing too much of the surrounding context. Without the . it would be possible to confuse this with an assignment. e.g. compare the following:
x = DroneEngine {
... // Maybe some more fields
health = if(condition)
y
else
z,
... // Maybe some more fields
};
// Could be easily confused with
var health = ...
...
x = DroneEngine: {
... // Maybe some more assignments
health = if(condition)
y
else
z; // The only character that's different might be many lines down
... // Maybe some more assignments
break :DroneEngine .{.health = health};
};
It’s essentially what most other languages arrived at, except for the .{ token, which seems to be a Zig invention.
E.g. the designated-initialization form { .bla = 123 } is from C99 (C99 has more flexibility though, e.g. you can do { .bla.blob.blub = 123 }, the common dot for both struct item access and pointer dereference is really quite common in modern languages and is also no longer controversial (I guess I’m alone missing the C -> syntax). The enum .bla is simply a shortcut for EnumName.bla, and so on…
E.g. most of Zig’s dot usages are also common in other languages, and I guess it’s better to follow established conventions than doing its own thing (especially when the alternative would be to add more symbols to the language).
Also the dot is simply a low-friction character, both when typing and reading. E.g. compare it to the noisy :: in C++ and Rust.
I’m with you there. I just like -> because it easily allows one to see pointer dereferences which are such a code smell, like foo->bar->baz->qux.
Also doing { .bla.blob.blub = 123 } is just so much better than .{ .bla = .{ .blob = .{ .blub = 123 }}}. Though then one needs to have defaults for all the other members, which, for better or worse, are getting to be frowned upon by the core team. Though I think some good design could still be found.
AFAIK “frowned upon” only for some uses cases, but I really hope the feature never goes away (because it’s incredibly useful for ‘option bag’ function arguments).
(however, I think a more flexible designated init syntax in Zig is definitely possible, it “just” needs to treat the designated-init syntax as its own ‘sub-syntax’ (especially for nested structs) instead of a ‘tree’ of inidividual structs which are initialized one by one recursively. At the end of such a specialized designated-initialization block parsing process make a sweep over the entire struct to check what individual struct members remain uninitialized, “fill the gaps” with their default values where defined, and what then remains uninitialized is a compilation error, e.g. even a complex nested struct should be initialized like a single flat struct where some struct items happen to be wrapped in a namespace, but are still technically part of the top-level struct).
But I think this sort of parser-special-casing doesn’t fit the basic design philosophy.
One thing that I don’t see mentioned yet here is greppability[1]. One of Zig’s goals is to be usable in a text editor without additional special tools (like ZLS).
The dot syntax is quite the helper here, since you can grep for something like \.hello\b and you’re sure to find member-or-decl accesses to hello. Of course, they’re not differentiated by the source, so if you have multiple hello members across multiple structs, the usefulness is slightly diminished. Still, it’s at least slightly narrowed down, so you get only member-or-decl accesses and not all occurrences of the identifier hello.
As an aside, when looking for where some identifier has been declared, you can narrow it down to a search of \b(var|const|fn) identifier\b. Much easier than C, where a declaration can start with anything because of typedef.
PS: I do use ZLS, but I’m at peace thanks to the fact that, if I for some reason weren’t able to, the hit probably wouldn’t be too hard thanks to Zig’s greppability.
The ability to use grep on it; making simple regex searches as useful as possible ↩︎
As far as I understand, syntactically, .something just means that the compiler will infer whatever is on the left (or sometimes, in place) of the . token. When it can be inferred successfully, it is ommitted.
So, .{} means “infer the type of the object {}”. And hence:
// given
const Foo = struct { x: u32, y: u32 };
const foo: Foo =
// Writing
.{ .x = 1, .y = 2 };
// Is like writing
Foo { Foo::x = 1, Foo::y = 2 };
// (which is not valid Zig, but you get the point)
Don’t worry about overusing it. Using it everywhere it is the direction Zig is headed anyways. Soon enough even T {} won’t be valid syntax, and the only way to create an object will be .{}.
One possibly simpler way to help understand the . is: generally speaking, this symbol is the trigger for LSP auto-completion.
Of course, the current ZLS is still immature, and there are some situations where this symbol fails to successfully trigger autocomplete. But overall, for the scenarios you mentioned that might confuse you, as long as you think about whether their positions are where you would expect autocomplete to trigger, you’ll realize that there’s some consistency to them.
var arr: std.ArrayList(u8) = std.array_list.Aligned.empty;
It can sometimes be a little confusing, buuuutt actually I do really like the syntax after a few weeks of writing Zig. Most of the time it’s been really nice not having to repeat information that the compiler already knows. Like the original example, makes sense. Obviously, .off has to come from EngineStatus, anything else wouldn’t make sense. Just takes some time to get used to it.
Worse, it’s more like std.array_list.Aligned(u8, null).empty. Technically, .empty is a member of an anonymous type returned by Aligned() so even calling the type Aligned(u8, null) is sugared.
ZLS seems to struggle with this too. I have vim set up so I can type gd (**g**o to definition) and it works on the symbol ArrayList, but not .empty.
(edit: Incidentally, I just found a bug in ziggit, too, in that a bold letter after a ( like (so makes it render **g**., but only in the above paragraph?)
Haha, of course you guys have examples much more complex than my simple engine. So the dot means the compiler can understand the only possible option there, and while learning I’d rely on the auto-complete, LSP to figure out the options I have at hand. OK. That .empty would point me to
const arr: std.ArrayList(u8) = .empty;
// const empty: Self = .{
// .items = &.{},
// .capacity = 0,
// }
//
// (Aligned(u8))
// Go to [Aligned](file:///home/mp42/Programs/zig/zig-0.16.0/lib/std/array_list.zig#L570)
//
// An ArrayList containing no elements.
But a leading dot does not always mean “figure out the type”, otherwise e2 here would work
const std = @import("std");
const print = std.debug.print;
const Engine = struct {
health: u8,
};
pub fn main() void {
const e1: Engine = Engine{ .health = 95 };
print("e1.health: {}\n",.{e1.health});
// const e2: Engine = Engine{ Engine.health = 12 };
// error: expected ',' after initializer
// print("e2.health: {}\n",.{21.health});
const e3: Engine = .{ .health = 90 };
// so ^ ^
// | |
// this means figure out the type
// |
// this one must mean something else then. (because of the error)
print("e3.health: {}\n", .{e3.health});
}
Does the dot in the print.{} also mean “figure out the type” ?
And .health is possibly borrowed syntax as mentioned
I like this!
Yes! and even the error is a bit cryptic
// temp.zig:14:9: error: local variable is never mutated
// var e: Engine = .{ .health=14 };
// ^
// temp.zig:14:9: note: consider using 'const'
// temp.zig:9:6: error: cannot assign to constant
// e.health += 1;
// ~^~~~~~~
local variable never mutated, and can not assign to constant… But I’m starting to see… the local variable was never mutated because health was not incremented, and += 1 failed because the parameter without the * is read as constant ? possibly.
It’s somewhat reassuring that I’m not alone being confused on the . used for omission, and maybe some would approve -> for pointer member access too.