10 ms·
I want to like Zig, but I also want operator overloading because multiplying matrices together in chains of functions is among the ugliest code I've ever had to
by adamdusty 5y ago
I want to like Zig, but I also want operator overloading because multiplying matrices together in chains of functions is among the ugliest code I've ever had to look at. There have been more than a couple decent suggestions for making it clean and obvious, but there are a few things that Andrew isn't willing to budge on.
> defer is pure genious
Being able to defer for resource cleanup is nice. I like that Zig makes it a priority to make things adaptable (not sure that's the right word). For example, everything in the std lib that allocates, takes an allocator object, so if you want to implement a custom allocator, it's very simple to use. If you want to use a custom event loop for async, it's easy to substitute. All in all, I think its shaping up to be a nice language, just not for me.
- majormajor 5y agoDefer sounds like a nice improvement over old old languages, but Java's try-with-resources is one of the nicer ways of handling resource cleanup I've seen in a "mainstream" language, it's fairly neat and conceptually similar to some stuff I've seen done with lisp macros or Ruby blocks. It seems like defer is still lacking compared to this.
- ptrwis 5y agoWith defer it's developer's responsibility to free resources in right order, when one depends on another. Maybe something like this in Zig would be better: try std.fs.cwd().openFile(path, .{}) |file| : file.close() { // ... code ... // file.close() is called at the end }
- dinglejungle 5y agodefer in Zig is per-scope, so { var file = try std.fs.cwd().openFile(path, .{}); defer file.close(); // ... code ... // file.close() is executed here } would work just fine.
- ptrwis 5y agoYes, but look at the example in the blog post of this thread- "stat" depends on "file", "file" depends on "path". You can call defer in any order in your code, the language syntactically doesn't enforce you to call defer right after resource acquisition. "try-with-resources" in Java (or "using" in c#) will always call "finalizers" in order opposite to how resources where created. Following the syntax I inveted above, for someone used to Java it would be easier to read than to think how defers are called: try std.fmt.allocPrint(allocator, "problems/{}", .{ id }) |path| : allocator.free(path) { try std.fs.cwd().openFile(path, .{}) |file| : file.close() { try file.stat() |stat| { try allocator.alloc(u8, stat.size) |contents| : allocator.free(contents) { // do something with 'contents' } } } } In Zig you can write this, and mess everything up: const path = try std.fmt.allocPrint(allocator, "problems/{}", .{ id }); var file = try std.fs.cwd().openFile(path, .{}); const stat = try file.stat(); var contents = try allocator.alloc(u8, stat.size); defer allocator.free(contents); defer file.close(); defer allocator.free(path);
- dnautics 5y agoThat's what errdefer is for.
- judofyr 5y agoI was actually thinking about writing a “macro” (i.e. function) which takes in a math expressions as a string and an anonymous struct for these use cases: eval("Ax + v", {.A = …, .x = …, v = …}) Wouldn’t that be cool?
- kristoff_it 5y agoThat's fairly easy to implement with comptime.
- mkishi 5y agoThat's indeed nice for math expressions. I'm a bit sad there seems to be no interest in adding interpolation syntax to Zig. While anonymous tuples work well for shorter expressions, they can get confusing for larger textual templates or DSLs, imo. I've been experimenting with Zig for WASM, for example, and the readability of HTML templates suffers. I'd love to be able to pass arguments inline, and while we can do that with anonymous tuples, the added noise makes it annoying to use in practice. I believe Javascript's template literals, out of all things, would be a fitting inspiration for a little syntactic sugar on top of anonymous tuples: var tuple_params = html.create("{} ... {} ... {}", .{a, b+c, observable}); var tuple_inline = html.create(.{"", a, " ... ", b+c, " ... ", observable, ""}); // equivalent to tuple_inline var tuple_sugar = html.create(`{a} ... {b+c} ... {observable}`);
- ifreund 5y agozig's standard library `std.fmt` functions support named arguments as well: var tuple_params = html.create("{[foo]} ... {[bar]} ... {[observable]}", .{ .foo = a, .bar = b+c, .observable = observable, }); It's verbose and explicit to be sure, but quite readable IMO.
- pron 5y ago> because multiplying matrices together in chains of functions is among the ugliest code I've ever had to look at. OK, but how much code is it? I doubt even a game engine has matrix operations in more than 0.1% of its lines of code (obviously, in Matlab/Julia it can by 99%). The beauty of so little code is a worthy sacrifice for the explicitness of a low-level language.
- kettlecorn 5y agoIt also comes up with vector addition, subtraction, and multiplication, all of which are used all over in gameplay code. I like Zig overall, but lack of operator-overloading just seems like a pain to me. Zig in many ways puts faith in the coder to not screw things up, but in this case it diverges from that philosophy. Would it be too much to trust the user not to overload operators in an unacceptable way? Social convention over technical constraint. That said I'd be more persuaded by the argument that it's not worth the extra code and complexity for a language as concise as Zig.
- flohofwoe 5y agoIMHO better than operator overloading (which opens the gates to hell) would be builtin vector and matrix types like in shading languages. Currently Zig has a vector type builtin (search for @Vector and/or std.meta.Vector), which supports the "simple" operations (addition, subtraction etc...), so it's halfway there, but no swizzle, and no matrix types.
- Hoppetosse 5y agoThe @Vector provided is actually a reference to SIMD vectors and there has been discussion to rename it to something that reflects this more clearly. https://github.com/ziglang/zig/issues/7305 https://github.com/ziglang/zig/issues/7305
- flohofwoe 5y agoI would love to see Clang's ext_vector_type extension features in Zig, it lets you write code like GLSL, including easy swizzling. Add the same thing for matrix types, and the the feature would be complete, no need for operator overloading: https://www.godbolt.org/z/zWGfcY5a4 https://www.godbolt.org/z/zWGfcY5a4 Whether this code translates to SIMD or scalar operations under the hood should depend on the target platform and compiler options.
- dom96 5y agoIf you haven't already then you might be interested in checking out Nim.
- Arnavion 5y agoHaving used defer in golang, I'm firmly in the camp that scope-based cleanup like defer is bad and value-based cleanup like destructors is better. Say you have this (pseudocode): function foo() { let bar = make_bar(); defer cleanup_bar(bar); let baz = make_baz(); defer cleanup_baz(baz); let quux = make_quux(); defer cleanup_quux(quux); do_something_with(bar, baz, quux); } Now imagine you have multiple functions like this that all create bar, baz and quux's. Say these are unit tests and the bar-baz-quux are mocks or whatever. So you decide to DRY by moving the creation to a common function. function create_mocks() -> (Bar, Baz, Quux) { let bar = make_bar(); defer cleanup_bar(bar); let baz = make_baz(); defer cleanup_baz(baz); let quux = make_quux(); defer cleanup_quux(quux); (bar, baz, quux) } ... except this doesn't work any more, because the cleanup happens within create_mocks and create_mocks ends up returning cleaned-up values. You could fix this by switching to a callback approach: function run_with_mocks(f: function(Bar, Baz, Quux)) { let bar = make_bar(); defer cleanup_bar(bar); let baz = make_baz(); defer cleanup_baz(baz); let quux = make_quux(); defer cleanup_quux(quux); f(bar, baz, quux) } ... but now you have to make the caller use a callback that necessarily returns void and not any other type. (This is mostly a golang problem, since languages with generics can be generic on the return type of the callback.) It also means you can't easily do things like set variables in an outer scope easily, depending on how the language deals with closure captures. Worse, if not all tests use the same mocks, you actually end up with multiple of these functions: function with_bar(f: function(Bar)) { let bar = make_bar(); defer cleanup_bar(bar); f(bar) } ... function foo() { with_bar(bar => { with_baz(baz => { with_quux(quux => { do_something_with(bar, baz, quux); }); }); }); } All these problems are avoided by having destructors.
- AndyKelley 5y ago> except this doesn't work any more it does in zig: fn create_mocks() !struct {bar: Bar, baz: Baz, quux: Quux} { var bar = try make_bar(); errdefer cleanup_bar(bar); var baz = try make_baz(); errdefer cleanup_baz(baz); var quux = try make_quux(); errdefer cleanup_quux(quux); return .{ .bar = bar, .baz = quux, .quux = quux, }; }
- gameswithgo 5y agoEvery day I see people avoiding entire interesting languages for bad reasons. Little things they are used to and don’t yet know how to work around the change, but you would learn and it would be fine. parens braces semicolons operator overloading generics etc the lack or presence of these things are never deal breakers, it can be worth trying to adjust because maybe you will find 900 things you love about the new way that make up for the one or two things you think you can’t stand.
- jolux 5y agoI’ve yet to find the perfect language but that doesn’t mean I’m satisfied with the imperfections that exist in the ones I use. It’s perfectly reasonable to not use a language for lack of operator overloading. I personally avoid languages developed with contempt for functional programming and type systems. We all have our reasons.
- ByteJockey 5y ago> the lack or presence of these things are never deal breakers Sure, but it can definitely break a tie. Frankly, operator overloading can create some really nice apis. And code that's pleasing to read takes less mental overhead to pick up.
- candeira 5y agoThe motivation for explicit allocation is explicitness, not adaptability. The adaptability is great, but it's a consequence of the language designer's design principle that all function calls will be explicit via (), and all allocation will be explicit via allocator-passing, so no user of a library can be surprised that a call performs an allocation.