Impressions
-
(2025-03-30)
Negative points
-
The LSP is TERRIBLE :
-
.
-
The parameters of
readAllAllocare:self,allocatorandsize. -
The parameters shown by the inlay hint are wrong, because it considers the first parameter to be
self, when in fact that is passed implicitly sincereadAllAllocis a method of a struct. -
statwasn't unwrapped in this case, so it will cause an error when trying to dostat.size-
In other words, it never informs me that I'm forgetting to do error handling.
-
-
-
It keeps showing me errors that have already been resolved after compiling and getting an error.
-
Even after fixing the error and even after restarting the LSP, it stays stuck showing old errors and ignoring the new ones.
-
-
-
Zig output messages are horrible and confusing .
-
Anonymous Structs are unpleasant .
-
Safety :
-
I don't like how anonymous structs are called; it always feels like I'm dealing with some Lua object.
-
There is NO indicator that I'm forgetting to initialize some required struct parameter.
-
The LSP doesn't try to infer which anonymous struct I'm referring to, to give me information whether things are correct.
-
Maybe that's very hard, since the struct is completely anonymous and could mean anything.
-
-
-
I don't like the syntax :
-
The declaration is done like this:
const Player = struct { pos: rl.Vector2, vel: rl.Vector2 = rl.Vector2.init(0, 0), acceleration: f16, vel_max: f16, sprite: rl.Texture2D };-
I find it confusing where the
structkeyword is placed.-
I'm creating a TYPE, but the impression I get is that I'm just creating a random
constthat receives an anonymous struct, but using thestructkeyword.-
Maybe deep down that's what is happening, but it's odd.
-
-
In Odin structs are declared the same way, but in Odin the
keywordfollows the same pattern as the declaration ofproc:Vector2 :: struct { x: f32, y: f32, }-
Proc in odin:
multiply :: proc(x: int, y: int) -> int { return x * y } -
It follows the same structure
name+::(const) +keyword(proc or struct) + syntax.
-
-
Zig seems undecided about it, such that it uses
var + name,const + nameandfn + namefor some things, but decides to invert the syntax for struct:const + name + = + struct.
-
-
-
But instantiation is done like this:
var player: Player = .{ .pos = rl.Vector2.init(0, 0), .acceleration = 100, .vel_max = 4, .sprite = player_sprite, };-
I don't like how instantiation uses
.in front of the names.-
I always forget to put them because it's just another small rule.
-
Maybe there is an argument about this, putting structs in a family similar to enums, but still it's annoying the difference between field definition and field access.
-
-
-
-
There are a zillion ways to instantiate an object :
-
Many of these ways result in bad behavior.
-
This is described in the Structs section.
-
-
-
Error handling :
-
It's necessary that I watch if the function return has
!in Zig.-
In Go or Odin I must watch if it's a double return, so the second return can be an error.
-
-
Using try everywhere is basically making an assert, but MUCH more confusing :
-
Under these conditions,
trydoesn't actually provide error handling. Thecatchprovides error handling, in my opinion. -
From what I see,
tryis simply annoying. -
Suppose what happens in Godot:
-
I try an operation that can return an object or null.
-
If it returns the object, OK.
-
If it returns null, then I may:
-
Assert if the object is null, where it should never be null.
-
Crash the code when trying to access some property or method of that object while it's null.
-
Check if the object is null, clearly deal with the consequences, returning from the function or assigning a default value to the object.
-
-
-
My options are absolutely clear.
-
-
THO, in Zig I feel there is a fourth option:
4. Automatically return the function and propagate that error to a higher function.
- Maybe that error is interpreted nicely in the upper functions, which is unlikely.
- Maybe that error simply keeps being propagated infinitely up to the main loop, which stops execution by forcing the main function to return with an error code.
- That is what happens in the vast majority of cases, since there's simply so much error handling happening in Zig, that 90% of them aretrys, out of laziness.
- In that case, it's basically an assert, but without guarantees that it will actually "assert" and stop the code. You need to traverse the whole call stack to find where error handling actually happens. -
tryis: "I don't want to deal with this error, I will pass the responsibility to whoever receives my return".-
As mentioned,
tryis just a shortcut forx catch |err| return err, which describes exactly what I said, which I find confusing to work with.
-
-
-
-
The std is terrible to understand :
-
.
-
ArrayList:-
It's a function that returns a type.
-
Calling
ArrayListis nothing more than callingArrayListAligned, but without alignment.
-
-
ArrayListAligned.-
It's a public function that returns a type.
-
It first receives a
typeT as a parameter and alignment information. -
It receives a comptime alignment, which is a
?u29, that is,nullor au29(29 bytes).-
Why is
comptimebefore the parameter name? I find that confusing. I prefer something likealignment: comptime ?u29.
-
-
-
Then the logic: if
alignmentis not null and the alignment equals T's alignment, then callArrayListAlignedagain, but this time with alignment equal to null. -
Then it finally returns a struct containing the type I want to define.
-
-
type
ArrayListAligned(T, alignment)-
Has 3 fields with no default value.
-
A
pub const Slicethat is not accessible outside the struct.-
I really have no idea why this field is not accessible.
-
The LSP doesn't give autocomplete and when trying to access ignoring the LSP I receive an error saying that this field doesn't exist, it's really strange.
-
-
A pub fn that returns a type
SentinelSlice(s). -
A pub fn that returns a type, where this type is
ArrayListAligneditself, that is, a "constructor" function.-
This returns the default values of each of the struct fields.
-
-
-
If the
init(constructor) ofArrayListAlignedonly does this, why weren't these default values already defined in the struct, instead of needing a constructor?-
It sounds like something done only by convention, but at the same time there were moments where this is not necessary or possible:
-
,
-
DebugAllocator is ultra common to be used, it has a total of 863 lines of code, with 6 fields in the struct.
-
It DOES have a
deinit(), but does not have aninit(). -
THO , reading the comment at the top of the function, it describes that
DebugAllocatordoes have an init.-
If you look well at the screenshot,
initis actually apub const init: Self = .{}. -
...what a mess.
-
-
-
Anyway, the way normally used is:
-
var debugAllocator = std.heap.DebugAllocator(.{}){}; -
or
var debugAllocator = std.heap.DebugAllocator(.{}).init;, if you want to use Zig's "convention".
-
-
-
-
The annoying thing is that trying to follow a pattern in Zig always produces these weird things, because of dealing with anonymous structs without LSP.
-
Much of the complexity is due to OOP; methods are really a pain.
-
All packages I've encountered are written in OOP, using methods for everything. It's simply mega confusing.
-
In the end, I never know when I should initialize the struct with default values using anonymous structs, or if I should call some init function that assigns default parameters to the anonymous struct.
-
-
-
String manipulation is unpleasant :
-
I find dealing with low-level memory fun. The theory is very interesting and I like having control over these things.
-
BUT, I feel Zig makes the whole process a pain, because of how the tools are provided.
-
.
-
In this small segment, it was necessary to use:
-
String comparison.
if (std.mem.eql(u8, info_tileset.get("identifier").?.string, "Internal_Icons")) continue;-
It sounds weird not being able to do:
if (u8, info_tileset.get("identifier").?.string == "Internal_Icons") continue; -
In Odin it's possible to do that.
-
-
String concatenation:
const complete_path: [:0]const u8 = try std.fmt.allocPrintZ(allocator, "{s}{s}", .{cwd, path}); defer allocator.free(complete_path);-
try-
Not the way I like to deal with errors.
-
-
std.fmt.allocPrintZ-
I have to access the standard library, the fmt package and use
allocPrintZ. -
I can't use
allocPrint, since I need the string to be null terminated.
-
-
"{s}{s}"-
Pass the string format, including
sto ensure it is interpreted as a string (as if that wouldn't already be obvious in this case).
-
-
.{cwd, path}-
Anonymous struct.
-
Anonymous structs are very strange.
-
Sometimes I interpret them as hashmaps, but in this case I could interpret as an array, or hashmap with unnamed keys.
-
-
Despite the confusion with the struct, this part is OK.
-
-
[:0]const u8-
I'm not sure.
-
-
-
-
-
-
Print is annoying :
-
I have to use
\nevery time, which I find annoying.-
There was never a moment when I didn't need
\nin print, never.
-
-
-
Ifs and loops use
( ):-
It's quite annoying to have to write
( )around the expressions of ifs and loops.-
I always forget and have to go back to fix it.
-
if (valor != 5) { return; } for (meu_array.items) |item| { std.debug.print("{any}", .{item}); } -
-
Unused parameters being an error is annoying :
-
Sometimes I just want to test something and see if it works, but every time I have to use
_ = a;or_ = .{a, b};. -
This is annoying after a while.
-
It also makes the code cluttered:
-
There will be extra lines of code that do absolutely nothing, they just take up space.
-
-
It also makes the code unsafe:
-
I would prefer it to be a warning, but this became NOTHING.
-
All information about the problem is lost, once I'm completely ignoring the problem.
-
-
In general, I don't like how Zig handles this; it's bad.
-
-
The
usingkeyword does nothing and will be removed :-
This is not a big problem, since I don't find
usingvery welcome, but it would still be ok to have this option for helper functions.
-
-
Constant breaking changes that make using external libs a headache
-
Many libs use incompatible versions among themselves.
-
Many libs use nightly versions, on master; others use older versions because they're not being updated.
-
Many libs even use dev, unstable versions.
-
The problem happens when there are changes in the build system from one version to another, so that it's not possible to use the lib at all unless you download its source code and edit it.
-
-
The build.zig and build.zig.zon are extremely confusing and inconvenient :
-
The LSP doesn't work if all dependencies are not correct in build.zig and build.zig.zon.
-
That's a pain for new projects.
-
-
I tried to learn about build.zig and despite having understood how to configure what I wanted, it's simply very confusing still. It's not friendly for beginners.
-
Maybe the configurations are useful when you master what's happening, but it's unpleasant to poke at those files at the beginning.
-
-
Doesn't do default initialization to Zero Value :
-
Doing
const flag: bool;gives the error "Variables must be initialized".-
This happens for everything.
-
-
Both Odin and GDScript do zero initialization for all types.
-
This is very nice, saves typing and makes the code safer by preventing trying to access an uninitialized property.
-
-
-
I find mandatory
;slightly annoying :-
Odin doesn't force it and I never missed it at any time.
-
-
Zig's syntax is VERY confusing :
-
No matter how much I use Zig, I feel like the next day I'm very confused about how the syntax works.
-
The entire std is just very confusing and the syntax is full of things I need to have memorized.
-
I reached this conclusion after trying Odin and realizing how much easier and more intuitive Odin's syntax is.
-
I have considerably fewer doubts writing Odin, although I barely read Odin's documentation, while I devoured articles and articles about Zig.
-
-
It sounds strange how unintuitive Zig can be syntactically.
-
I feel Zig has some "fussy bits" that make the syntax one of the most confusing I've used.
-
Rust, Odin, Swift, Jai, etc.; are all syntactically more intuitive than Zig.
-
-
Positive points
-
deferworking in scope is nice .-
I have big doubts about this, though.
-
Go makes
deferwork at the end of the current function scope, which may be better for error handling.-
I found it strange to use
deferinsideifs, as it makes the call happen at the end of theifscope, which is odd and impractical.
-
-
On the other hand, it's nice to create blocks with internal defers:
{ //something here }
-
-
Pointers are nice :
-
I kinda like the usage of
.*for dereferencing.
-
-
Many things are expressions, instead of only statements :
-
I'm unsure whether I like this so far.
const s = 'a'; const valor: i32 = switch (s) { 'a' => 1, 'b' => 20, 'c' => 5, _ => 0, }; -