Most active commenters
  • flohofwoe(5)
  • jsheard(4)
  • adastra22(3)
  • (3)

←back to thread

Zig is hard but worth it

(ratfactor.com)
401 points signa11 | 65 comments | | HN request time: 1.397s | source | bottom
1. jsheard ◴[] No.36150389[source]
I get what Zig is going for in making all operations as explicit as possible, but I fear that it's going to turn away fields like graphics and game development where it would be a good fit except for the lack of operator overloading forcing you to go back to C-style math function spaghetti. It's all fun and games until what should be a straightforward math expression turns into 8 nested function calls.
replies(8): >>36150475 #>>36150541 #>>36150795 #>>36150946 #>>36151013 #>>36151746 #>>36151792 #>>36152172 #
2. bodge5000 ◴[] No.36150475[source]
I'm no expert on zig, but the one area I have seen it shooting up in popularity is game dev. Though I guess that is largely as a replacement for C, so "C-style" wouldnt be much of a concern
replies(3): >>36150560 #>>36150748 #>>36151102 #
3. TwentyPosts ◴[] No.36150541[source]
I am not much of a Zig-head, but the best compromise I can think of is having a few operators which purely and solely exist for this purpose. In other words, there is no operator overloading, but you can define, say, "#+" for any two structs, or something like that.

So if you want to encode matrix multiplication, then you'll always have to write `mat1 #* mat2`. This feels like a hack, and isn't all that elegant, but it'd be clear that every usage of such an operator is a disguised function call. (And according to what Andrew Kelley said, it's all about not hiding function calls or complex operations in seemingly innocent 'overloaded' symbols.)

If you want to take this one step further you'd probably have to allow users to define infix functions, which would be its own can of worms.

Honestly, I am not particularly happy with any of these ideas, but I can't think of anything better either!

replies(3): >>36150784 #>>36153058 #>>36153273 #
4. jsheard ◴[] No.36150560[source]
Zig looks like a fine C replacement, but C isn't what people are using to make games in the vast majority of cases. It's all C++, and operator overloading is part of the "sane subset" that everyone uses even if they hate the excesses of modern C++ as a whole.
replies(1): >>36150721 #
5. felixgallo ◴[] No.36150721{3}[source]
as a long time game dev, I actively don't want operator overloading. That's some spooky action at a distance nonsense. I'm not sure I have seen a codebase that involved operator overloading, either, and I've worked in or near a good quantity of well-known titles.
replies(3): >>36150843 #>>36151267 #>>36160066 #
6. pjmlp ◴[] No.36150748[source]
Not really, it is mostly around communities like Handmade, most studios couldn't care less and it doesn't fit most engines that they are using.
replies(2): >>36161455 #>>36161478 #
7. ljlolel ◴[] No.36150784[source]
Idea: allow some weird Unicode operators like Julia does. Then it’ll be clear the weird operator is doing something weird and new. And this already works in other languages. There are lots of Unicode
replies(1): >>36151239 #
8. audunw ◴[] No.36150795[source]
I like Zigs justification for not having general purpose operator overloading (no hidden function calls and loops)

But I wish they added:

1. Ability to declare completely pure functions that have no loops except those which can be evaluated at compile time. Something with similar constraints as eBPF in other words. These could be useful in many contexts. 2. Ability to explicitly import overloaded operators, that can only be implemented using these pure guaranteed-to-finish-in-a-fixed-number-of-cycles functions.

Then you'd get operator overloading that can be used to implement any kind of mathematical function, but not all kinds of crazy DSL-like stuff which is outside the scope of Zig (I have nothing against that, I've done crazy stuff in Ruby myself, but it's not suitable for Zig)

9. jsheard ◴[] No.36150843{4}[source]
There's no accounting for taste, but the two major publicly available engines (Unreal and Unity) both use operator overloading in their standard math types.
10. pron ◴[] No.36150946[source]
As someone who works on another language that is relatively reluctant to add language features (Java) we regularly face such dilemmas. A user shows up with a problem that could be helped by the language. The problem is real and a language feature would work, but there are many such problems, and adding features to solve all of them will make the language much bigger, overall causing greater harm (even those who don't themselves use the feature need to learn it to be able to read code). What we try to ascertain is how big of a problem it is, how many programs or lines of code it affects, and is there possibly a single feature that could solve multiple problems at once.

So I would ask you this: what portion of your program suffers from a lack of user-defined infix operators and how big of a problem is it overall? Even if it turns out that the problem is worth fixing in the language, it often makes sense to wait some years and then prioritise the various problems that have been reported. Zig's simplicity and its no-overload (not just operator overloads!) single-dispatch is among its greatest features, and meant to be one of its greatest draws.

replies(2): >>36153538 #>>36161441 #
11. sigsev_251 ◴[] No.36151013[source]
To be fair, there is an operator overloading proposal for C2y/C3a and there is at least one compiler that offers operator overloading as an extension.
12. mr_00ff00 ◴[] No.36151102[source]
Would love to see zig in game dev. I’ve tried some rust and while I love rust in general, I find game dev in it a bit of a mess.
replies(2): >>36152719 #>>36158754 #
13. spenczar5 ◴[] No.36151239{3}[source]
Writing greek symbols is sufficiently annoying that I always kind of resent code that does this. It’s not just about the first time you are writing code, but also when you are reviewing it, or trying to share a snippet with a coworker, or lots more scenarios. Maybe it’s just me, but writing ‘z = x ∇ d’ is really tedious.
replies(2): >>36153390 #>>36156259 #
14. Taywee ◴[] No.36151267{4}[source]
You'd rather use explicit function calls for all linear algebra and geometry operations? I don't think adding two vectors using an overloaded + is that spooky or distant.
replies(1): >>36152896 #
15. tuhats ◴[] No.36151746[source]
I have played around with using @Vector for linear algebra.

This removes the need for operator overloading for a vector type, which covers most use cases of operator overloading and I often in fact think is the only legitimate use case.

I don't get to use `*` for matrix multiplication, but I have found I do not mind using a function for this.

I have only been playing with this in small toy programs that don't to much serious linear algebra and I haven't looked at the asm I am generating with this approach, but I have been enjoying it so far!

16. kapperchino ◴[] No.36151792[source]
For the math stuff you can do things like a builder pattern where you can flatten the nested functions. But operator overloading is definitely preferred
17. flohofwoe ◴[] No.36152172[source]
Zig has a builtin @Vector type that might come in handy for most cases where in C++ a math library with operator overloading would be used:

https://www.godbolt.org/z/7zbxnncv6

...maybe one day there will also be a @Matrix builtin.

replies(3): >>36152928 #>>36153017 #>>36154505 #
18. adastra22 ◴[] No.36152719{3}[source]
Have you tried bevy? I’m starting with bevy for a non-game project, but I’m blown away by how simple it is to use once you get used to the magic.
replies(2): >>36153938 #>>36154063 #
19. flohofwoe ◴[] No.36152896{5}[source]
FWIW Zig can do that without operator overloading: https://www.godbolt.org/z/7zbxnncv6
replies(2): >>36154682 #>>36154931 #
20. jsheard ◴[] No.36152928[source]
That still breaks if you layer any abstractions on top, for example if you wanted to build an AoSoA packet of Vec3s you'd have to define an addition of those in terms of a function.

https://www.godbolt.org/z/v8Ta8hEbv

Zig is exactly the kind of language where you'd want to build a performance-oriented primitive like that, but AFAICT the language doesn't let you do it ergonomically.

replies(1): >>36153332 #
21. throwawaymaths ◴[] No.36153017[source]
Probably not. @Vector is not a mathematical vector, it's SIMD. it makes sense because there are times when those live in registers and a poly fill for stack memory isn't burdensome.

@Matrix makes less sense because when it gets big, where are you getting memory from?

replies(1): >>36153241 #
22. renox ◴[] No.36153058[source]
> I am not much of a Zig-head, but the best compromise I can think of is having a few operators which purely and solely exist for this purpose. In other words, there is no operator overloading, but you can define, say, "#+" for any two structs, or something like that.

And those operators wouldn't have any precedence.

> If you want to take this one step further you'd probably have to allow users to define infix functions, which would be its own can of worms.

As long as these infix function are preceded by a recognizable operator ("#" in your example), I think that this would be fine.

23. flohofwoe ◴[] No.36153241{3}[source]
For 'game-ey' math code, a matrix is at most 4x4 floats (64 bytes), that's fine for a value type that might live on the stack.

vec2..4 and matching matrix types up to 4x4 is basically also what's provided in GPU shading languages as primitive types, and personally I would prefer such a set of "SIMD-y" primitive types for Zig (maybe a bit more luxurious than @Vector, e.g. with things like component swizzling syntax - basically what this Clang extension offers: https://clang.llvm.org/docs/LanguageExtensions.html#vectors-...).

replies(2): >>36154544 #>>36159575 #
24. thechao ◴[] No.36153273[source]
Maybe an operator-overloading region?

    #{
       m3 = m1 * m2 + m3;
       m3 += m4;
    }
Basically, pure syntactic sugar to help the author express intent without having to add a bunch of line-chatter.

Speaking of operator-overloading, I really wish C++ (anyone!) had a `.` prefix for operator-overloading which basically says "this is more arguments for the highest-precedence operator in the current expression:

    a := b * c .+ d;
Which translates to:

    a := fma(b, c, d)
replies(1): >>36154201 #
25. flohofwoe ◴[] No.36153332{3}[source]
That's true, but OTH that's already getting into territory where operator overloading might start to become detrimental because it hides important implementation details which might not be expected from a simple '+'.
26. kps ◴[] No.36153390{4}[source]
∇ is near-worst-case since it's not even Greek. I think domain-specific keyboard layouts are as much of a good idea as language-specific layouts, but they're a nuisance to install on *nix (trivial on OS X). Using .XCompose is the most practical *nix approach, in the absence of program-specific methods like Julia's tab-completable backslash names.
replies(2): >>36154436 #>>36156298 #
27. markisus ◴[] No.36153538[source]
In robotics, numerical linear algebra expressions comprise a large part of the code base. If not by line count, then definitely by the amount of time spent writing, reading, and debugging such code. This makes Zig unusable for these applications, at least not without additional tooling. You can get a feel for how unergonomic this is by avoiding the use of all arithmetic operators in your code and instead forcing yourself to use user defined plus(a,b), minus(a,b), assign(a,b), etc, or programming directly with the C blas api.
replies(3): >>36154016 #>>36154155 #>>36159570 #
28. the__alchemist ◴[] No.36153938{4}[source]
Bevy isn't on the same level as tools like UE and Godot.
replies(3): >>36154350 #>>36155653 #>>36176465 #
29. hellcow ◴[] No.36154016{3}[source]
Using plus(a,b) for complex types sounds fine to me… I have to imagine if I were working with a huge file of these for a while it would start to feel normal, just like every language has a different syntax but you eventually feel comfortable using.
replies(1): >>36157498 #
30. mr_00ff00 ◴[] No.36154063{4}[source]
Maybe I need to give bevy a second go. My big issue is I felt I was learning to speak “bevy” instead of using rust. A lot of functions I wrote required queries of components, but the queries were built and called behind a magically wall.

I don’t have much game dev experience though outside of simple games using libraries like raylib to just move and draw stuff. Maybe once things get complicated enough they are all like bevy.

replies(1): >>36156019 #
31. erichocean ◴[] No.36154155{3}[source]
> You can get a feel for how unergonomic this is by avoiding the use of all arithmetic operators in your code and instead forcing yourself to use user defined plus(a,b), minus(a,b), assign(a,b), etc, or programming directly with the C blas api.

You've dramatically overstated your case, since that's true of every Lisp-like language.

Lisp is a perfectly suitable language for developing mathematics in, see SICM [0] for details.

If you want to see SICM in action, the Emmy Computer Algebra System [1] [2] [3] [4] is a Clojure project that ported SICM to both Clojure and Clerk notebooks (like Jupyter notebooks, but better for programmers).

[0] https://mitpress.mit.edu/9780262028967/structure-and-interpr...

[1] Emmy project: https://emmy.mentat.org/

[2] Emmy source code: https://github.com/mentat-collective/emmy

[3] Emmy implementation talk (2017): "Physics in Clojure" https://www.youtube.com/watch?v=7PoajCqNKpg

[4] Emmy notebooks talk (2023): "Emmy: Moldable Physics and Lispy Microworlds": https://www.youtube.com/watch?v=B9kqD8vBuwU

replies(1): >>36157461 #
32. MH15 ◴[] No.36154201{3}[source]
Huh I've never seen this approach. Very interesting solution, could be adapted to the JavaScript matrix libraries I bet.
replies(3): >>36154476 #>>36154965 #>>36157284 #
33. Ygg2 ◴[] No.36154350{5}[source]
To be fair Godot isn't on same level as UE.
34. Conscat ◴[] No.36154436{5}[source]
I interpret this as meaning that ibus is difficult to set up. FWIW, Emacs and Kitty let you input unicode without it.
35. thechao ◴[] No.36154476{4}[source]
This is how TeX handles math — the "$" operator is an "inline" version of the same.
36. Conscat ◴[] No.36154505[source]
It's a LOT worse than C++ SIMD libraries.

In C++ (EVE, Vc, Highway, xsimd, stdlib), you can specify the ABI of a vector, which allows you to make platform specific optimizations in multifunctions. Or you can write vector code of a lowest-common-denominator width (like 16 bytes, rather than specifying the number of lanes), which runs the same on NEON and SSE2. Or you can write SIMD that is automatically natively optimized for just a single platform. These features are available on every notable C++ SIMD library, and they're basically indispensable for serious performance code.

37. cmbothwell ◴[] No.36154544{4}[source]
You might like Odin, it has a similar philosophy to Zig and supports swizzling:

https://odin-lang.org/docs/overview/#swizzle-operations

Matrix types are also built in:

https://odin-lang.org/docs/overview/#matrix-type

I’ve thought for a little while that Odin could be a secret weapon for game dev and similar pieces of software.

replies(1): >>36155740 #
38. kprotty ◴[] No.36154682{6}[source]
Vectors in Zig are SIMD types. Vectors in games are probably algebraic types. Using SIMD for the latter may not be that useful if 1) specific elements are accessed frequently 2) transformations involve a different operation happen on each element.
39. cyber_kinetist ◴[] No.36154931{6}[source]
First of all that only works with vectors, but I want operator overloading to also work on things like matrices or custom types (for example quaterions, or a symmat3 struct that represents a symmetric 3x3 matrix using only 6 floats).

Additionally, for efficient math code you often want vector / matrix types in AOSOA fasion: for example Vec3<Float8> to store an AVX lane for each X/Y/Z component. I want vector/matrix operations to work on SIMD lanes, not just for scalar types, and Zig currently can't support math operators on these kinds of types.

replies(1): >>36155544 #
40. estebank ◴[] No.36154965{4}[source]
In Rust you could use a proc macro that parsed the block and translates the token to a new token stream that uses method calls with the appropriate operator precedence, for arbitrary operations you could want to define. You're effectively writing a compiler plugin and language extension at that point. For targeted niche domains, this might be worthwhile.
41. felixgallo ◴[] No.36155544{7}[source]
I have a marvelous proof that you can solve that with comptime but unfortunately the margins of this website are too small to contain it.
replies(3): >>36156503 #>>36161875 #>>36165689 #
42. adastra22 ◴[] No.36155653{5}[source]
Depends on what you're doing. I'm writing a non-game app that requires a scenegraph, raycast mouse selection tool, and other tools of the sort typically required by games and provided by a game engine. But there's a lot of game stuff I don't need, and I need to make major customizations to the rendering engine. It ended up being easier to implement in bevy, due to its modularity, than it would have been in UE4 or Godot.
43. flohofwoe ◴[] No.36155740{5}[source]
I'm actually dabbling with Odin a bit in the scope of language bindings for the sokol headers:

https://github.com/floooh/sokol-odin

It's a very enjoyable language!

44. adastra22 ◴[] No.36156019{5}[source]
This is true on both fronts, I think. Bevy magics away the interface between you and the engine via the ECS macros, in a way that is very unusual for a systems programming language like Rust. But that's more or less how all game engines are these days from what I understand.
45. ◴[] No.36156259{4}[source]
46. lvass ◴[] No.36156298{5}[source]
Just use C-x 8 in Emacs and you'll get any symbol by name, with autocomplete.
47. lvass ◴[] No.36156503{8}[source]
Do us a pastebin, please.
48. bakkoting ◴[] No.36157284{4}[source]
There's a not-very-active proposal to add operator overloading to JS which takes a similar scoped approach:

https://github.com/tc39/proposal-operator-overloading

49. KerrAvon ◴[] No.36157461{4}[source]
Why do you think this is true of Lisps? Emmy would not seem to be a good example because it actually does overload arithmetic operators to support extended data structures. Look at, for example:

https://cljdoc.org/d/org.mentat/emmy/0.30.0/doc/data-types/m...

replies(1): >>36157586 #
50. KerrAvon ◴[] No.36157498{4}[source]
Are you sure you would find this more ergonomically pleasing:

assign(x, plus(a, plus(b, plus(c, b))))

When you could have:

x = a + b + c + d;

Or:

(let x (+ a b c d))

?

replies(1): >>36161750 #
51. erichocean ◴[] No.36157586{5}[source]
> forcing yourself to use user defined plus(a,b), minus(a,b), assign(a,b)

This is the complaint I was responding to. Here is that code in Clojure (a Lisp):

    // What the GP claims is bad for doing math:
    plus(a,b)
    minus(a,b)
    assign(a,b) // <= I have no idea what this does, or has to do with math.
    
    // Let's actually use the original math operators, but with function notation:
    +(a,b)
    -(a,b)
    
    // And here's the Clojure/Lisp syntax for the same:
    (+ a b)
    (- a b)
Lisp doesn't have "operators", so it doesn't have "operator overloading." What it does have is multi-dispatch, so yeah, the implementation of `+` can depend on the (dynamic) types of both `a` and `b`. That's a good thing, it means that the `+` and `-` tokens aren't hard-coded to whatever the language designer decided they should be in year 0, with whatever precedence and evaluation rules they picked at the time.

The point I'm making is that you absolutely DO NOT need to have special-cased, infix math operators to "do math" in a reasonable, readable way. SICP is proof, and Emmy is a breeze to work with. And it turns out, there are a lot of advantages in NOT hard-coding your infix operators and precedence rules into the syntax of the language.

replies(1): >>36158723 #
52. markisus ◴[] No.36158723{6}[source]
By assign(a,b) I meant to denote the copying of the matrix b into the matrix a. The ability to use arithmetic symbols `+` and `-` as function identifiers definitely aids in readability but I don't think this is possible in Zig.

I am not familiar with Emmy but I'm guessing that the usual work flow will involve an interactive shell with many calls to `render` to display expressions in infix notation so that you can better check if the expression you typed is actually what you meant to type.

The infix notation, although arbitrary and not as logically simple as other notations, is almost universal in the math-speaking world. Most mathematicians and engineers have have years of experience staring at infix expressions on blackboards, and disseminate new knowledge using this notation, and do new calculations in this notation.

In reading your reply, I think that maybe some tooling that could auto-insert corresponding infix-like comments above an AST-like syntax could be a solution for writing such code in Zig.

53. i_am_a_peasant ◴[] No.36158754{3}[source]
I gave it a go with openGL and Rust, was great until I needed some external C++ libraries.. Then I lost motivation and rewrote the whole thing in C++
replies(1): >>36158882 #
54. shrimp_emoji ◴[] No.36158882{4}[source]
Name checks out

<33

replies(1): >>36159160 #
55. i_am_a_peasant ◴[] No.36159160{5}[source]
absolutely
56. anonymoushn ◴[] No.36159570{3}[source]
You can use = for structs at least :)
57. throwawaymaths ◴[] No.36159575{4}[source]
Sure, but people are going to want to use operator overloading for, e.g. dl/ml
58. chlorion ◴[] No.36160066{4}[source]
>That's some spooky action at a distance nonsense.

In pretty much all languages operators are just sugar for calling a method. There is no difference other than an easier to read syntax.

In rust for example, doing a + b is exactly the same as doing a.add(b).

In python it's exactly the same as doing a.__add__(b).

In C++, my understanding is that its sugar for a.operator+(b) or operator+(a, b).

I think there are some arguments against operator overloading but "spooky action at a distance" doesn't seem to be a very good one to me.

59. QQ00 ◴[] No.36161441[source]
>and adding features to solve all of them will make the language much bigger, overall causing greater harm (even those who don't themselves use the feature need to learn it to be able to read code).

I agree that adding too many features can make a language too large and bloated. However, I disagree that this is always the case. For example, adding features that make it easier to code math is not necessarily a bad thing. In fact, it is a good thing, as it can make programming more accessible to a wider range of people.

Additionally, math is often used in fields that require high speed, such as computer graphics and game development, Computer vision, Robotics, Machine learning, Natural language processing (NLP), Mathematical modeling, all kinds of scientific computing (Computational physics, Computational chemistry, Computational biology...) As a result, low-level programming languages are often used to implement the core code in these fields. As you see, Math is essential for many fields.

60. ◴[] No.36161455{3}[source]
61. ◴[] No.36161478{3}[source]
62. chrsig ◴[] No.36161750{5}[source]
is assign(x, plus(a, b, c, d)) not an option?
63. QQ00 ◴[] No.36161875{8}[source]
You can use pastebin, or even better, use github gist.
64. another2another ◴[] No.36165689{8}[source]
I think you have briefly deluded yourself with an irretrievable idea.
65. nextaccountic ◴[] No.36176465{5}[source]
Zig gamedev libraries aren't either