11 ms·
Zig, Skia, Clojure, Geometry and the Japanese TV Show: ICFP Contest 2021
- jedisct1 5y agoGreat write up and well done!
- adamdusty 5y agoI 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.
- timClicks 5y agoThis is a excellent write-up. I'm really impressed how quick it was to whip up a UI for manual solving. I would argue however that Rust counts as a functional programming language, however. Algebraic data types, higher-order programming and immutability are all idiomatic features of the language.
- dgb23 5y agoI was impressed by that too. It is a very valuable skill, to be able to build a UI prototype that quickly. The author attributes it to Clojure and there is certainly truth in that, but given he also basically learned a new programming language in a day and got a decent rating on his solution makes me think it is not just the tool. For one, this is a smart and skillful programmer. But it also takes a playful mindset and training to be able to do this.
- ifreund 5y agoThe fact that the author was able to find success with Zig so quickly speaks not only to the author's abilities but to the simplicity of Zig. Learning enough C++ or Rust to be productive in the same amount of time would be a much greater challenge.
- whateveracct 5y agoRust is missing the function part though. For good reason - closures and GC are like PB&J. But programming exclusively with closures like you can in Haskell or Scheme isn't as feasible in Rust. This isn't to say Rust doesn't have excellent closures, lambdas, etc given its design constraints.
- elias94 5y agoGreat article, despite the size of that burger! Just one question: why he says that clojurescript is a pain in the ass? I know he used it in multiple projects and wrote datascript library compatible.
- Scarbutt 5y agoJust one question: why he says that clojurescript is a pain in the ass? Because you have to deal with a lot of BS for no gain over modern JS.
- elias94 5y agoSorry, what you mean with BS?
- nmfisher 5y agoI've never used Clojurescript, but I have played around with Fable (F# to JS transpiler), and from memory I had a bit of a headache with transpiling non-framework dependencies and getting it to talk to Javascript libraries (e.g. Plotly). It wouldn't surprise me if Clojurescript was the same. It's by no means impossible, but at the end of it you do wonder if the benefits of the language outweigh the hassle of getting things up and running.
- mrtweetyhack 5y agoawesome read and insights
- skavi 5y agoThe “dark mode” on this website is cute. I wonder if it might help some people read.
- 1MachineElf 5y agoSame for the portrait shown on the lower right at the end of the article. Hover your mouse over it.
- mkishi 5y agoThat's cute too and I'm now convicted it should do the opposite when "dark mode" is on.
- fiddlerwoaroof 5y agoI assume this is a commentary on the desirability of dark modes.
- urthor 5y agoI think it's a tool for ADHD? It's quite adorable.
- cxxforwin 5y ago"C++ was ruled out as no fun"" I'm disappointing with the author for not trying Modern C++ for himself instead trusting the "others". I trusted the "others" (Eg Linus and Richard Kenneth Eng) and missed JavaScript and C++ for so long. Now, I use both of them. Never trust "others". Try yourself and you will see how amazing those languages are. You fought with borrower checker with Rust but in C++ you don't have to. With Zig, you have to manage the memory manually but with C++ you don't have to thanks to the RAII If you went with C++, you probably have saved several hours of your time. Zig is cool but I am not gonna to use it near time soon unless they add overloading. I understand they want to keep the language simple. But this same simplicity itself gonna to hurt the language in future when used in large applications. "what you read is what you get" is what actually failed C in large applications. It's impossible to know everything in whole world. We have to learn to appreciate abstractions.
- pjmlp 5y ago> Participating in 2019 ICFPC taught me one thing: it’s hopeless to do number-crunching and brute-forcing in anything but C/C++/Rust. Except the first price column clearly shows that is false. It is quite obvious it was more a skill of the developer getting the first prize than the language itself.
- jhgb 5y ago> Except the first price column clearly shows that is false. Only if you assume that "number-crunching and brute-forcing" is the only way to the first prize, surely?
- pjmlp 5y agoExactly the point, it isn't.
- cassepipe 5y agoSo we can decoralate winning the first prize and brute forcing. OP is saying language matters if you want to brute force and not say, win the first prize.
- underdeserver 5y agoBut the statement from TFA talks about number crunching and brute forcing, not about winning first place.
- renox 5y agoI'm not convinced that Zig is the best 'low lovel' language for such kind of context: it's more targeted for'embedded' use when you need 'absolute' control. A compiled language with a GC like D or Nim seems more productive..
- gavinray 5y agoWhy is this getting downvoted? Have written code in all the languages mentioned and the performance characteristics of them are not seriously different enough to warrant the decrease in productivity if this was the goal. The clear usecase for Zig is in resource-constrained environments (it produces the smallest binaries I've ever seen) or when you want a very strict, Rust-esque type system. I'm not saying you can't use it outside of that, or that it isn't a great language, but that as far as how many braincells and lines of code go in to solving a problem in Zig vs D vs Nim and very different for what is generally a small amount of performance. (Also both of those languages have no-GC modes, just FWIW)