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.
this is surprising to me! the last library i used frequently that had a bunch of types which wanted to be pinned and/or intrusive was libxev, which deliberately did not do what you say. one of my iterations of using it used a bunch of calls to create(), which made me happy to discover std.heap.MemoryPool(T) (i think that’s what it’s called?), but a later iteration found this to be suboptimal.
Im pretty sure std is not avalible in global namespace in most languages. What is available is a prelude which is just a bunch of useful stuff from std or its a language builtin (like python’s print).
Regarding builtins zig actually has a bunch of those. But they are on lower level side compared to something like pythons’s print. Problems with builtins is what they should work on all platforms supported by the language and for example on embeded there are no terminal where you can print.
So better question would be “why not add some kind or prelude module which is imported by default”.
Pre-1.0 Zig has mostly been removing features (async, usingnamespace, etc), and avoiding adding new ones. So if you’re asking what’s planned in the direction of more implicit behavior or syntactic shortcuts before 1.0, the honest answer is probably nothing.
And you are right, the language can feel more verbose than e.g. Odin, and you should be honest about what approach is important for you - since it’s your tool so you decide what feels best to you.
To me I value the explicitness of Zig, our language-creators have made many good decisions that have allowed for this without concurrently scaling up its verbosity, but trying to fulfill both these boxes also does shrink the space for how the programmer can/should express themselves.
This is also why I remembered that for the first 6 months - Zig felt stale or stiff and hard to mold for me - so there is time required in just trying to develop the feeling or the ‘knack’ of the language.
It seems to me that it is fundamentally important here not to represent the standard library as a single whole.
For example, it doesn’t occur to me yet what problems might arise anywhere with std.meta.Child or std.meta.Elem. But there are many more such completely independent parts of the standard library.
Ahhh yeah great catch, my bad on that. If we really went in the weeds we could do something like make(.array_list, T, alloc, 2) or make(.slice, T, alloc, 2), but I’m sure that goes very against the whole Zig philosophy.
Can you expand on why global functions wouldn’t work properly because of the missing builtins?
It’s a problem of initialization. Zig’s ArrayList is implemented in userland and so the compiler doesn’t know how to initialize it.
The language could be changed to add constructors or rely on naming conventions to know what method to call.
But, such changes will never happen as they’ll complicate the language specs and following naming conventions is something we already do. No need for the compiler to care about that.
Oh and allocators are also part of userland so to make(make) work the language needs to define the allocator interface, which is more language additions I doubt will happen.