Posted this on Reddit and was referred to this site instead. Putting this here for any thoughts from the community / any explanations as to why my ideas are crap haha
Does anyone know if Zig has any more planned language semantics to be introduced pre 1.0?
A couple things I’d personally love to see:
#1
make()
free()
destroy()
into the global namespace ala Odin / Golang
#2
std.mem.Allocator std.Io
const std = @import(“std”) avail by default
Same as above but less passionate about these
#3:
.tag = val instead of .{ .tag = val } syntax for tagged unions
#4 Default function params
potentially for implicit allocator use ala Odin
I understand that a lot of the goal of Zig is to make things as explicit as possible, I find that there is a lot of unnecessary typing in the syntax and in function signatures that makes the language a little more verbose than I’d like. Curious if any of these are either planned / maybes / or never gonna happen.
I believe that as you use zig more, you will understand why language suggestions are not accepted for now, but everyone suggests their own favorite features anyway.
This is a bad idea for libraries, as it removes choice and control from the caller, preventing optimal software. For your own non-library projects, if working off of a global allocator improves your code then it’s perfectly fine to do so.
A few lines at the top of your file to write this out is fine.
Man, that’d be nice. However the ideas falls down when you try to work through the implications on parsing.
Mostly this is a bad idea that leads to bad code. Sometimes it’s a good idea instead to take a single argument which is a struct that has default values. See, eg std.Build.Module.CreateOptions
Mostly, it sounds like you just need to be fine writing out a little bit more. Code is read far more than it is written, so that use case is optimized for. Being explicit helps reading and understanding code. Optimizing for keystrokes is one of those ideas it’s better to just let go of, things get a lot more pleasant once you do.
Great point re default parameters in a struct. Will be using that for sure.
#1:
I didn’t mean that those functions would take a global allocator but rather the functions would be available without having to import std and get a valid allocator instance before create() is in scope.
#2:
Would there be any downsides to making these avail as they seem to be used as params in over 50% of functions
i think the point of needing to have an allocator is making explicit the need to consider an allocation strategy. these suggestions make that need less explicit, so i think they’re unlikely to land.
I get that regarding implicit allocators in function signatures and default params, but the make() / destroy() functions I am suggesting would still require an explicit allocator to be passed it would just help unify creation / deletion of allocated types.
ex.)
std.ArrayList(T).initCapacity(alloc, 2)
vs
alloc.create([2]Foo)
could be simplified to
make(std.ArrayList(T), alloc, 2)
and
make(Foo, alloc, 2)
The reason to add a make method is when additional initialization is required.
In zig normally we call them init when returning a struct or create when returning a pointer to struct.
Yes of course I didn’t mean to wrap that exact function in a new signature, but rather to replace the underlying implementation with a function that is not a behavior of the allocator type.
And got it re the std not being auto included for embedded. I know absolutely zero about embedded so taking your word for it.
There is a misunderstanding on how zig works in your example.
std.ArrayList(T).initCapacity(alloc, 2) and make(std.ArrayList(T), alloc, 2) are not equivalent.
The former will initialize a struct where a field items will be assigned the value of alloc.alloc(T, 2). The latter (assuming go make semantics) will allocate a slice with two std.ArrayList both uninitialized.
alloc.create([2]Foo) will create a single pointer to an array of 2 elements and arrays in zig are values. make(Foo, alloc, 2) would be the same as alloc.alloc(Foo, 2) a slice of two elements.
I don’t think the unification you hope for can work in zig beyond what the allocator interface offers. As zig doesn’t have builtin dynamic array and hash map, so such global functions won’t work properly.
If these things impact your user experience significantly, it means you are dynamically allocating too often.
3 out of the 4 suggestions are about introducing things implicitly. Zig has this absolutely beautiful feature, which makes it one of the most readable languages: every name has a declaration in that file.
If you see foo = 3; anywhere, you can guarantee there is a var foo in that same file, which explains exactly what foo is. Coupled with the absence of shadowing, it makes everything obvious and ergonomic, specially when coupled with “go to definition” from an LSP.
By far, the worst, most frustrating experience of using C and C++ is not being able to figure out what type something is, because it’s hidden in 1 #include out of 1000, and even your LSP has given up.
It’s quite inevitable for libraries trying to provide a relocatable handle-like values when you try to keep stable references in inner object graph. Otherwise it leads to fragile unsafe API. stdlib could take a stance “just use it correctly” but it’s rarely an option for other libraries.
It’s funny how programming in Zig made me finally internalize why move-semantic is such an important topic.
Regarding why many of the std APIs are not in the global namespace, and why there are no wildcard imports, you might be interested in this article:
In short, it reduces context in the parser and allows the compiler to make more assumptions on a per-file basis. The compiler can know more detailed information about the dependencies between files, and can prevent needing to re-parse entire files, modules etc for incremental updates.