Zig's (.{}){} Syntax
openmymind.net
openmymind.net
Here is the code to illustrate:
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
var visited = std.BufSet.init(arena.allocator());
var bw = std.io.bufferedWriter(std.io.getStdOut().writer());
const stdout = bw.writer();
So.. what are the entities we use, conceptually? "allocatorButNotReally" and "thisTimeReallyAnAllocatorIPromise"? Same for the writer?Plus, the documentation isn't much explaining wtf is this and why.
The answer is probably buried somewhere in forums history, blogs and IRC logs, because there must have been consensus established why is it ok to write code like that. But, the lack of clear explanation is not helping with casual contact with the language. It's rather all-or-nothing - either you spend a lot of time daily in tracking all the media about the ecosystem, or you just don't get the basics. Not good IMO. (and yes I like a lot about the language).
https://ziglang.org/documentation/master/std/#std.mem.Alloca...
File and Socket both offer a `.writer()` function to create a writer interface bound to a specific concrete "writeable stream".
BufferedWriter has both extra state (the buffer) and extra functions (flush) that must be part of a concrete implementation separate from the writer interface.
> The answer is probably buried somewhere in forums history
That's just how computers work, languages that don't expose these details do the same exact thing, they just hide it from you.
Well, your explanation doesn't really tell why do I call .deinit() on a structure before alllocator() call and calling all the rest important stuff on a structure after such call. I think you guys, while doing great job by the way, are kind of stuck in a thinking from inside language creators' perspective. From outside, certain things look so weird.
I need also to be a picky about "that's just how computers work" phrase, you know uttering such a phrase has always a danger of bumping into someone who wrote assembly before you were even born and hearing this makes a good laugh..
That's because the Allocator interface doesn't define that an allocator must be deinitable (see in the link above the fn pointers held by the vtable field). So just like you have to call flush() on a BufferedWriter implementation (because the Writer interface doesn't define that writers must be flushable), you have to call deinit on the implementation and not through the interface.
Fun fact, not all allocators are deinitable. For example std.heap.c_allocator is an interface to libc's malloc, and that allocator, while usable from Zig, doesn't have a concept of deiniting. Similarly, std.heap.page_allocator (mmap /virtualalloc) doesn't have any deinit because it's stateless (i.e. the kernel holds the state).
https://www.openmymind.net/Zig-Interfaces/
which says that an interface is essentially just a struct that contains pointers to methods. In other words when you call the .thing() method on your SpecificThing, that method is producing a Thing that knows how to operate on the SpecificThing, because functions that accept Things don't know about SpecificThings. You can't manufacture that Thing without first having a SpecificThing, and a SpecificThing can't be directly used as that Thing because it's not. There's essentially no other way to do this in Zig.
This is explained right in the documentation about arena allocator. Arena allocator deallocate everything at once when it goes out of scope (with defer deinit()). You need to call .allocator() to get an Allocator struct because it's a pattern in Zig to swap out the allocator. And with this, other code can call alloc and free with out caring about the implementation.
This is just how arena allocator works and not related to Zig's design. You may take issue with how Zig doesn't have built-in interface and having to resort to this implementation struct returning the interface struct pattern, but I think the GP clearly explained the Why.
Zig doesn't have those, so you're forced to use these ad-hoc struct instances.
The zig allocators used to use this because it enabled allocator interfaces without type erasure, but it was found to have a minor but real performance penalty as it is impossible for any compiler to optimize for this in scenarios that are useful for allocators.
Other interfaces might actually have the opposite performance preference
I think that can be expected of a pre-1.0 language ecosystem though. Currently it's more important to get the language right first and then worry about cleaning up the stdlib APIs.
The most important job of std is not (contrary to popular belief) to provide a “bag of useful high quality things” but rather providing interfaces and types that 3p packages can use without coordinating with each other. I’d argue that http.Handler, io.Reader/Writer/Closer are providing the most value and they are just single method signatures.
When there’s universal agreement of what shape different common “things” have, it unlocks interop which just turbo charges the whole ecosystem. Some of those are language, but a lot more is std and that’s why I always rant about people over focusing on languages.
var gpa: std.mem.GeneralPurposeAllocator(.{}) = .{}; var gpa: std.mem.GeneralPurposeAllocator(.{}) = .init; var gpa: std.mem.GeneralPurposeAllocator(.{}) = .init;
Which finds the declaration literal `std.mem.GeneralPurposeAllocator.init`, a pre-declared instance of the GPA with the correct starting configuration.In other words, for a function f(x : T), f(.{}) is f(T()), not f(), where T() is the default constructor for type T.
If we had a function with two parameters g(x : T, y : T2) it would be g(.{}, .{}) which means g(T(), T2()), not g().
It looks like the feature exists to avoid things like:
x : really_long_type = really_long_type(), which can be replaced with x : T = .{} to avoid unnecessary duplication.
They should add default parameters to avoid this sort of thing. Maybe they ought to consider named/labelled parameters, too, if they're so concerned about clarity.
Per the article, this is the explicit form:
var gpa = std.heap.GeneralPurposeAllocator(std.heap.GeneralPurposeAllocatorConfig{}){};The fact that unacceptable parameter would fail compile time validation does not make it any more readable.
var foo = OpaqueTypeName(.{}){};
What is the . eliding?You don't know. I don't know. It's impossible to tell because the type is opaque to our understanding.
Zig is a static language without variadic parameters, so you can't make it optional in that sense. You could make the options a `?T` and pass `null` instead, but it isn't idiomatic, because passing `.{}` to a parameter expecting a `T` will fill in all the default values for you.
Arity N when you supply a value
Arity N-1 when you use the default
I can think of a few reasons
- makes function calling simpler
- faster compilation
- less symbol mangling
- makes grepping for function implementation easier
If for some reason you think you absolutely can't live without variadic functions, maybe don't use zig.
i don't know if there are other uses for . and {} that would make this hard to read. if there are, then maybe that's an issue, but otherwise, i don't see that as a problem. it's something to learn.
ideally, each syntax element has only one obvious use. that's not always possible, but as long as the right meaning can easily be inferred from the context, then that's good enough for most cases.
const a_struct: StructType = .{ .foo = "baz"};
As well as union initialization. So .{} is a struct constructor for a struct where every field has a default value, which chooses all of those fields. const a_struct: StructType = StructType{ .foo = "baz" };
const a_struct = StructType{ .foo = "baz" };
const a_struct: StructType = .{ .foo = "baz" };It's really not much different than in nearly any other language with type inference except for the dot (which is just a placeholder for an inferred type name).
Obviously if you remove lifetimes, types, references, etc. you're going to need less syntax.
I don't think this is the case. Firstly, all the necessary data can be encoded by keywords, spaces and newlines. Forth or TCL can encode everything Rust can (since their interpreters are 100% configurable), only with keywords, and spaces between. A language should have special syntax for only the important part, not for everything.
Secondly, even though Rust has special syntax for a lot of stuff, it could be nicer to the eye.
For example, RattleSnake here https://matklad.github.io/2023/01/26/rusts-ugly-syntax.html or "Haskell flavored Rust" here https://news.ycombinator.com/item?id=34541695#34543124 are much nicer to the eye.
But if you need a bunch of stuff that may or may not apply to a function or type definition for example, then why not just use CSS/Rebol style syntax, and put all your keywords that apply in a row. No need for all the weird symbols, brackets, colons and all that. You could even use keyword=no, and be extra explicit.
Also IMO the Rattlesnake example looks awful. The Haskell flavoured Rust is even worse. Do you seriously prefer those? If so I'm afraid your sense of taste is a bit suss.
But that kind of shortcut seems to be optional.
As far as simplicity, I think their pitch is "simpler than Rust", not in absolute terms. The whole comptime thing is hardly simple in general.
simple <-> complex
easy <-> difficultIn the same vein as what you said here about orthogonality ( https://news.ycombinator.com/item?id=42097347 ), programming languages and IDEs should be orthogonal (and actually are, unless deliberately linked). People were using languages much before IDEs existed. And they got a hell of a lot done using the primitive surrounding tools that existed back then, including, you know, gems like Lisp and the concepts embodied in it, many of which have, much later, been adopted by many modern languages.
And I still meant "almost compel", even by the community, because of course they cannot really compel you. I meant it in the sense of, for example, so many people using VS Code for programming posts.
It's ironic that you complain about this because Zig is probably the most "normal editor" friendly programming language for exactly the kind of thing mentioned in the article.
I don't need an IDE to figure out the 12 options to that function and fill them out with the correct defaults. I don't have to hunt through 23 layers of mysterious header files to find the declaration I need to figure everything out. etc.
Just try figuring out a foo(12).bar(14).baz("HELP!").fixme("ARRGH!") construction chain in C++ or Rust without an IDE. Oof.
1) Zig doesn't encourage those and 2) in Zig I can trace the @import() calls and actually run "grep" on things.
echo Who complained, $(echo bsder | sed 's/sd/ro/') ? ;)
Not me. Don't put words into my mouth.
(I don't care if I got the above shell syntax wrong), this was just a quickie, for fun ;)
you seem to have misunderstood my words, in the exact opposite way from what I meant. congrats. not!
>I don't need an IDE
who told you that I needed an IDE?
chill, willya?
and, wow:
>to figure out the 12 options to that function and fill them out with the correct defaults. I don't have to hunt through 23 layers of mysterious header files to find the declaration I need to figure everything out. etc.
12 and 23, exaggerating much? we are not talking about win32 API functions, podner.
>Just try figuring out a foo(12).bar(14).baz("HELP!").fixme("ARRGH!") construction chain in C++ or Rust without an IDE. Oof.
Don't resort to theatrics or histrionics to make your point (like HELP! and ARRGH!), (I am allowed to, tho, because i > u :)
>1) Zig doesn't encourage those and 2) in Zig I can trace the @import() calls and actually run "grep" on things.
faaakkk!
though a bsder, you find header files mysterious, and cannot grep through them, if they are in C++ or Rust, eh? are find and xargs your enemies? or even just ls - R | grep ?
stopped editing, even though there might be a few minor typos.
now, fire back! :)
[0] - https://github.com/zigtools/zls
[1] - https://kristoff.it/blog/improving-your-zls-experience/
Similar to when you add a new enum member and it complains of all switch statements that are not using it (as long as you didn't add a default case).
fn example() {
let p = returns_a_point_type(args);
}
Where create_point() is a function from a module (e.g. not even defined in that file) which returns the Point type automatically inferred for p? I mean sure, it's technically constructed in the called function... but is that often a useful distinction in context of trying to find all of the places new instances of types are being assigned? In any case, this is something the IDE should be more than capable of making easier for you than manually finding them anyways. fn Node(T: type) type {
return struct {
value: T,
next: ?*Node(T) = null,
};
}
In other languages, defining types in terms of themselves is unproblematic, because the type identifier is just a symbol and the whole thing amounts to a graph with a backreference.However, here it's supposed to represent actual executable code, which is run by the compiler and "produces" a type in the end. But how would the compiler execute this function without getting stuck in a loop?
However, I also wouldn't be surprised if somehow the memoization in the zig compiler makes this ok
It's just regular comptime function calls which happen to have parameters or return values which are comptime type values (types are comptime values in Zig). I find that a lot more elegant then inventing a separate syntax for generics, and it lets you do things trivially which are complex in other languages (like incrementally building complex types with regular Zig code).
It might be unusual when coming from C++, Rust or Typescript, but it feels 'natural' pretty much immediately after writing a few lines of generic code.
If I had to define it, I would go with runtime-only, but I could see the other way too.
Perhaps Zig is the language that on purpose blurs the line between what runs when (by basically having what looks like macros integrated into runtime code without any conspicuous hashtaggy syntax), and so a Ziggy would not see compile-time control flow as something weird.
const c = blk: { const x = 5; break :blk x-3; }; // c = 2
just having an empty block `{}` is exactly that—an empty block of type `void`. having a dot or something else distinguishing it from a block is necessary in order for it to not be that. fn my_func(): MyType {
return .{ ... };
}
The dot is just the placeholder for the inferred type and the above is equivalent with: fn my_func(): MyType {
return MyType{ ... };
}
...and Zig allows to write that verbose form too if you prefer that.But while trying out some things and learning the language I find it annoying. And I don't know how it makes me more disciplined when I can just write `_ = unused;` to suppress the error. Even worse, if I forget that assignment in the code the compiler will never warn me about it again even when I want it to.
So far I haven't seen any upside to this.
I wrote a 16kloc Zig project a couple of months ago, and not once the 'unused variables are errors' thing was annoying (https://floooh.github.io/2024/08/24/zig-and-emulators.html)
IME unused variables being (linter) errors also isn't all that unusual in the Javascript/Typescript world. It feels 'normal' fairly quickly.
> I wrote a 16kloc Zig project a couple of months ago, and not once the 'unused variables are errors' thing was annoying
That's great, but different people are different. I've tried learning Zig twice by now but this is just a dealbreaker, simple as that.
My workflow is probably very different from yours I'm guessing. I have my editor configured to save on every keystroke and I have a watch process that then recompiles my code. I pretty much never manually compile. My compiler is sufficiently fast that I almost never wait more than 1 second to build and run my code. I notice every error immediately and fix them as they arise. This is what I am talking about with discipline. I never allow my code to get into an unexpectedly broken state and don't need a linter since I just make the compiler as strict as I would make the linter. This ultimately simplifies both the compiler and the build pipeline.
These are all huge upsides for me. The cost of occasionally deleting a definition and then restoring it are for me minor compared to the cost of, say, writing a separate linter or adding feature flags to the compiler (the latter of which doesn't fit into my workflow anyway since I auto compile).
a = input()
b = f(a)
c = g(b)
d = h(c)
If you delete the unused variable d, then c will be unused, so you’ll have to delete it too. Iterating this, you will end up deleting the entire code.- comment it out
- use git
- use ctrl-z
Maybe 1/6 if you're debugging.
In theory, programmers can just be disciplined enough or set up CI lints for unused imports. In practice…
we accept type inference but don't do the same for module references? why?
pike does have an import statement, but its effect is to include all members of a module into the namespace instead of just resolving the ones that are really used. and instead of speeding things up, using import on modules with lots of members may actually slow things down because the namespace is loaded up with lots of unused references. sometimes import is used to help readability, but that's rarely needed because you can solve the same with a simple assignment to a variable.
if you can show me an example where import resolves an ambiguity, i'll try to show how pike solves the problem without import.
In C++, you have to #include the thing you want to use somewhere, not necessarily in the same file you use it, it just has to end up in the same compilation unit. If two files define the same name, you'll end up with a compilation error. In very large projects, sometimes you won't notice this until some transitive dependency includes the other thing.
I'm personally a fan of explicit imports. I imagine this helps IDEs resolve references without having to scan 1000s of files to resolve them, and it helps build tools pull in only the needed files. Regarding speed (of execution), in JS we have tree-shaking so if you import a file but don't use all of its members, those excess/used members will be dropped from the final bundle (saving on both bundle size and run-time parsing). Plus it means I don't have to spend much time thinking of a super/globally unique name for every thing I define.
import math
foo = math.pi
the compiler can obviously find math in the import statement. why can't it find math.pi directly?pike can.
And how does this work in practice? In Python and JS you can also put executable code inside your modules that gets executed the first time it's imported. Are you telling me that that's going to run the first time it's implicitly imported instead? Is that a good idea?
The story in JS gets even crazier because you can do all kinds of whacky things with imports, like hooking up "loaders" so you can import things that aren't even JavaScript (images, CSS, you name it), or you can modify the resolution algorithm.
depends on the language, in pike, and as far as i know in python i still can use it as a variable if i want to, it would just cover the module and make the real datetime inaccessible. but why would i want to do that? if i see someone using a well known module name as a variable in python i would probably recommend changing it.
i don't see the benefit of not filling the global namespace over making import unneeded. add to that, by convention in pike module names start with an uppercase letter, and variables don't, so the overlap is going to be extremely small and never causes any issues.
In Python and JS you can also put executable code inside your modules that gets executed the first time it's imported
pike doesn't have that feature. if there is something to be initialized you'd have to call it explicitly. i don't see a problem with that, because in python you call import explicitly too. so you just swap out one need to be explicit for another. i prefer the pike way because it actually gives me more control over if and when that initialization happens.
i think that also better fits the paradigm of explicit is better than implicit. in python i have to learn which module does initialize stuff or ignore it, in pike i can easily see it from the way it is used.
further in pike to get an initialization i would normally create an instance of a class in the module because modules are singletons, and you probably don't want to change something in them globally.
going to run the first time it's implicitly imported instead
pike is compiled, that is, all these references are resolved first, before any code is run. so even if there were any implicit initialization it would be possible to run it first.
more specifically, pike modules are instantiated objects. i don't know the internals, but i believe they get first instantiated when they are resolved. again, that's before the rest of the code where the reference came from is running
Sure, saying "an open paren must have a matching close" is quantitatively different from "a def must have at least one matching ref", but is it really qualitatively different?
fans are indisciplined. ;)
_ = foo; // autofix
If you like copying dead memory around, you can always do `_ = unusedParam` to confirm to the compiler that you don't need that variable after all, despite going out of yiur way to declare it.
https://ziglang.org/devlog/2024/#2024-11-04
It seems like an interesting idea, but I wish Andrew spent more time fleshing it out with complete examples. I can't tell if the _ characters are eliding values or if that's literally what's in his code.
The underscores mean that it's a non-exhaustive enum. An exhaustive enum is where you list all the names of the possible enum values, and other values are illegal. A non-exhaustive enum means any value in the underlying storage is allowed. So at root this code is creating a bunch of new integer types which are all backed by u32 but which can't be directly assigned or compared to each other. That means you can't accidentally pass a SectionIndex into a function expecting an ObjectFunctionImportIndex, which would be impossible to do if those functions just took raw u32's.
Is that the idea?
You can do the same thing wrapping integers with structs, but enum makes it slightly more concise?
I think in Zig for new types you'd use enums for ints and packed structs for more complex types.
Are you sure Rust has variadic generics? Last I checked, the extent of progress was a draft RFC. https://github.com/rust-lang/rfcs/issues/376
Man, must be nice to be a time traveler from the 2030s. How does Covid25 turn out?
f(.{}) is calling a function, which takes a struct, using an in-place constructor which provides the default arguments for the necessary struct type.
f({}) is passing an instance of `void` to a function with a `void` type parameter. Do you need to do this, specifically? No. But you can.
fn voidPass(v: void) void {
_ = v;
}
test "void pass" {
voidPass({});
}Adding an entry requires you to pass a void value:
map.put("hi", {});Standard C has no such feature as blocks being expressions.
Compound literals are grotesque. The braces have to be proceeded by cast syntax indicating the type. It could be subject to inference. Maybe the current draft has something.
Parentheses, brackets and braces I find can help a lot in guiding your eyes though code. They make it more „regular“, or lessen the „entropy“ (physicists, please don‘t stone me).
For this reason I‘ve come to dislike Rust’s braceless if-syntax, after being a die-hard fan. With parentheses it just reads better.
my_func((my_type){ .x = 1, .y = 2 });
my_func(&(my_type){ .x = 1, .y = 2 });Every single time, I am telling you, every single time I must write an anonymous struct using that dot, I am becoming totally confused!
Me too buddy, me too!
Edit: I should put an /s here, am I right?