Anyone have working experience with Nim and Zig? I'd love to hear how they are similar and contrast. I'd also would like to see some idiomatic web server benchmarks between the two (now with Nim v2).
I've used both to work on a hobby OS project (Nim[1], Zig[2]). I very much prefer Nim. Code is succinct, elegant, and lets you focus on your core logic rather than fighting the language.
Zig is nice and I like its optionals support and error handling approach. But I was put off by its noisy syntax, e.g. !?[]u8 to represent an error union of an optional pointer to a many-pointer of uint8. Also having to prepare and weave allocators throughout most of the code that needs to dynamically allocate (which is most of the code) gets in the way of the main logic. Even little things like string concatenation or formatting becomes a chore. Zig also doesn't have dynamic dispatch, which makes polymorphic code hard to write; you have to work around it through some form of duck typing. In the end I realized that Zig is not for me.
Couldn't edit my post, but forgot to mention my main pain points with Nim have been:
- its module system, especially not being able to have mutually recursive imports (there has been a 7 year old proposal[1])
- order-sensitive declarations of procs (i.e. can't use a proc defined further down in the file unless you add a forward reference to it). For the latter there's an experimental pragma[2], but it doesn't work a lot of times once you introduce mutually recursive calls
- object variants requiring declaration of a separate enum instead of allowing inline declaration of the variant cases, and a close issue[3] with not being able to define the same field names under different variant cases.
If I recall correctly, lazy symbol resolution, which would allow both circular module imports and order-independent procs, was initially on the roadmap for 2.0. Currently, it was moved to a stretch goal for 2.2.
I maintain auto-generated bindings for my C libraries for Zig and Nim (and Odin and Rust - although the Rust bindings definitely need some love to make them a lot more idiomatic).
I think looking at the examples (which is essentially the same code in different languages) gives you a high level idea, but they only scratch the surface when it comes to language features (for instance the Zig examples don't use any comptime features):
And which language did you enjoy coding in the most? Yeah, a subjective question :-). (Edit: Missed the auto-generated part, so maybe you don't have an opinion on experience regarding this?)
I think actually Odin, although I was surprised by that (being more of a Zig fan). Odin has some neat convenience features which are nice for higher level code, while Zig can be a lot more 'draconian' by enforcing correctness even at the cost of some "line noise" (but I guess both languages are still in flux, so that might change).
As for Nim I enjoyed it initially (because of the Python vibes I guess) but the automatic memory management gets confusing quickly. IIRC there's quite a few different reference types - but maybe that has been simplified in 2.0
PS: Even though the bindings are auto-generated, I still try to make them 'language-idiomatic' by injecting some 'semi-manual' mappings for things like naming conventions or implicit type conversions, ideally getting the API close to what a 'native' API would look like - at least that's the goal.
I’ve written programs in both, though it’s been a while since I used Nim now.
I think I enjoyed writing Nim more. Zig is more boring, but for all the right reasons. I wouldn’t personally choose to write an OS in Nim, but I think Zig would be great for that when it’s mature. I personally started using it for embedded software.
I would probably use Nim for CLI tools, server applications, maybe GUI applications and games too.
The Zig teams seems to be putting much more effort into the whole compiler infrastructure, which is really amazing in my experience. There’s some great innovations there.
I suspect Nim would be much, much harder to wrangle for games than Zig (or easily the best of the bunch: Odin) since it doesn't make enough things clear at all in terms of allocation and only allows indirect control of allocation and deallocation.
I wouldn't necessarily prefer Nim for any of the things you listed but this doesn't have the same argument as for games with Odin (which has great tools and libraries for making games as well as gives a much better overview of important things you'll have to care about for making them in terms of performance, etc.).
Rather, it's because I've found that Nim belongs with the other languages that think that complexity can be managed by being hidden well enough, which I've found is simply not the case when something actually needs to be debugged or you need to understand the behavior of the program.
Hiding/ignoring allocation errors, not making allocation explicit, not making deallocation explicit, etc., makes for a much worse time actually understanding what's going to happen. Adding tons of GC options like alternative GC implementations isn't going to fix it and this new one is really just another example of trying even harder to hide complexity.
I think the ultimate irony of these languages that have magical features like move semantics is that they do some of those things in the name of performance but in practice many of them are so complicated to write well-performing code in with these space technology features and non-obvious behavior that the end results are worse than much, much simpler languages. I've also found that these languages' development cycles (for the end user) isn't that much longer than the space tech ones because there is ultimately much, much less to use in them so people end up just writing the actual code instead of trying to wrangle all of the magic.
Many game developers want to focus on writing games instead of fighting a memory allocator. Unless you're making a 3D game with realistic graphics, you don't need every last bit of performance.
No one needs to be fighting allocators; they are far less inhibiting and pose less of an issue than GC or RAII will in the vast majority of cases. They're far easier and simpler to deal with on the whole as well. You're always interfacing with memory management somehow and the implicit way is usually much harder to work with overall. The idea that having an allocator and explicitly working with it is for "that last bit of performance" is a bit disingenuous, you're usually losing far more than that with implicit allocation and deallocation. On top of that you simply inherently have a harder time understanding the behavior of your program.
Manual memory management is one more thing to care about instead of the actual logic. With automatic memory management, you don't need to think about memory at all; what could be simpler?
Easier in the best case and much harder in the worst, when your lack of thinking is an issue (which it definitely will be unless you're prepared to use more of the machine for no reason). Simplicity is not about what's easier to use, it's about how you interface with something, how simple and straight forward that interface is to use, how many things are implicitly or explicitly affected by that thing, and so on. Automatic memory management usually implies an assumption that allocations can't fail, memory is infinite, etc., so the assumptions and complications are many. It also adds more code you didn't write and have no direct control over, which complicates your problem solving in many ways.
GC or other automatic memory management is only easier if you have absolutely zero care for resource usage. RAII will oftentimes lead to single allocations and deallocations, for example, unless you take care to not have it be so, which is an immense waste of resources.
It's fine if you don't care and you know that that's going to produce slow, bad software, but let's be honest about that instead of saying you can not care and everything will be fine.
There were loads of specific differences, but if I could characterize both languages in a simple way:
- Nim seems to emphasize being a swiss army knife in the way that Python is, except as a compiled language.
- Zig is a much more focused language that tries to hit a certain specific niche - being a successor and replacement for C - and hits that mark spectacularly.
I think language preference comes down to what your personal needs and wants out of a new language that isn't being served by whatever you're using currently. I personally landed in the "Zig" camp because the way it approaches its ambition of being a C successor is intriguing, but I could see why other people might land on Nim.
It is maybe the most simple web server implementation, similar to what you get from "python3 -m http.server"? What sense does it make to compare highly focused web server frameworks to languages most simple stdlib implementations, much apples vs oranges.. (thus also not getting why the proposal to compare with actual web frameworks for nim is that much downvoted?!)
Nim's default json library is terrible in performance, but there're much faster drop-in replacements like jsony[1]. I'm not sure that's the main issue for low rank, but it's definitely one of them.
I would not call std/json it "terrible in performance" probably still way faster then what you get in many other languages (like python). But yes the JSON lib I wrote is faster due to avoiding branches and allocations.
Interesting. The Vercel benchmarks make it look pretty good. Only slightly behind rust. https://programming-language-benchmarks.vercel.app/zig-vs-ru... Benchmarks are as much about the skill of the programmer as they are about the language. I suspect those numbers could improve drastically.
Possibly it wasn't compiled with `-d:release`. I only looked briefly — is there a way to see the source code and cli flags used for the various implementations?
It isn't really a language community so interested in web server efficiency, and until recent years threading efficiently was kind of tricky with the GC scheme they used. If someone wanted Nim to rank high you could do it, but I'm not sure it is worth the effort?
There are a lot of features in Nim that are basically the polar opposite to Zig's values; macros/templates as opposed to comptime which has no real capability of just inserting random code and the very pervasive naked imports (functions/methods can come from anywhere) that are all over the place come to mind, as opposed to the explicit imports and qualified names you would have to use in Zig (or deconstruction of imports to get the bare names, making it obvious where an identifier is coming from).
On top of that you have only indirect control over memory allocation and deallocation, which goes completely against Zig's values where custom allocators are used and everything that allocates should take an allocator as an argument (or member in the case of structures). In contrast to that there isn't even the concept of an allocator in the Nim standard library.
I would say that my experience with Nim has made me fairly certain that Nim has absolutely no desire to make things obvious but rather chooses convenience over almost everything. It's not so much a competitor (in performance or clarity) to Odin or Zig as it is a competitor to Go or something with a much higher-level baseline.
On top of all of this it doesn't really have tagged unions with proper support for casing on them and getting the correct payload type-wise out of them, which is an incredibly odd choice when all of its competitors have exactly that or an equivalent.
Overall I would say that coming from Odin or Zig (or Go) and actually liking those languages it's very hard to like Nim. I could imagine that if someone came from a much higher-level language where performance is nearly inscrutable anyway and nothing is really obvious in terms of what it's doing, Nim would feel like more of the same but probably with better performance.
Edit:
Often while reading the Nim manual, news and forum posts, etc., I get the sense that Nim is really just an ongoing research project that isn't necessarily trying to solve simpler problems it already has along the way. If you look at some of the features in this announcement, it's hard to see anyone ever asking for them, yet here they are. In many ways it's way worse than Haskell, which often gets derided as "just a research language". A lot of what Nim has makes for a much worse experience learning and using the language and I'm sure it doesn't get easier in the large.
> It's not so much a competitor (in performance or clarity) to Odin or Zig as it is a competitor to Go or something
That seems accurate. Dealing with raw pointers as one does in Odin or Zig is very much de-emphasized in favour of dealing with safe references, and a lot of effort is put into optimizing out all the overhead of those reference checks (hence ARC/ORC) and writing code to evade them. The manual memory management features of Nim are there for flexibility and fallbacks and are not really the main way to write code: even for embedded. The stuff that Zig (and Odin?) do surrounding allocators and alignment, and constructs for slightly-safer pointers, are really very interesting yet are most helpful if you are indeed working with pointers and worrying about offsets: which you usually aren't in Nim.
I am curious as to what you mean about comptime, though. I have gotten the impression that equivalent constructs in Nim are more powerful. You have `static` blocks and parameters, `const` expressions, `when` conditionals, and then also both templates and typed macros operating on the AST (before or after semantic checking)... `when` even provides for type-checking functions with varying return types (well, monomorphized to one type) via `: auto` or the `: int | bool | ...` syntax.
I will also defend "naked imports" as a feature that works very well with the rest of the language: functions are disambiguated by signature and not just name and so conflicts scarcely occur (and simply force qualification when they do). And, this allows for the use of uniform function call syntax - being able to call arbitrary functions as "methods" on their first parameter. This is incredibly useful and allows for chaining function calls via the dot operator, among other things. Besides, if you really want you can `from module import nil` and enforce full qualification.
> I will also defend "naked imports" as a feature that works very well with the rest of the language: functions are disambiguated by signature and not just name and so conflicts scarcely occur (and simply force qualification when they do). And, this allows for the use of uniform function call syntax - being able to call arbitrary functions as "methods" on their first parameter. This is incredibly useful and allows for chaining function calls via the dot operator, among other things. Besides, if you really want you can `from module import nil` and enforce full qualification.
This is spot on. You can also not really have productive and well-fitting errors-as-values in a language that emphasizes UFCS, which is why Nim (and D) has/have to have exceptions. In order to productively use errors as values in Nim you either have to chain some kind of `Result` type (which, if you `map` & `mapError` over it will have to be able to implicitly allocate in certain cases, etc.) so the list of potential victims of this (and other features) just seems to go on and on.
In general, if you go over the list of features in Nim there is a coherence in them only in that some of the (mis)features actually have to exist in order for other features to make sense. I would feel like it was "designed" except in the case of Nim it really feels mostly accidental and not very well though out in general. The end result is (for me) that it feels very much like it ended up on the wrong side of readability, clarity and overall coherence.
> You can also not really have productive and well-fitting errors-as-values in a language that emphasizes UFCS
Eh, https://github.com/arnetheduck/nim-results and associated syntax from https://github.com/codex-storage/questionable would beg to disagree. Nim's stdlib does not have productive and well-fitting errors because it suffers from inertia and started far before the robust wonders of recoverable error handling via errors-as-types entered the mainstream with Rust and were refined with Swift (IMO). Option/Result types are fantastic and I do so wish the standard library used them: but it's nothing a (very large) wrapper couldn't provide, I suppose.
I do strongly think that other languages are greatly missing out on UFCS and I miss it dearly whenever I go to write Python or anything else. I'm not quite sure how you think UFCS would make it impossible to have good error handling? Rust also has (limited, unfortunately) UFCS and syntax around error handling does not suffer because of it. If by errors-as-values you mean Go-style error handling, I quite despise it - I think any benefits of the approach are far offset by the verbosity, quite similarly to Java's checked exceptions.
(in general concerns surrounding performance of errors surprise me - they're errors! they shouldn't be hit often! but if they are, you can certainly avoid such performance hits in nim.)
Zig is nice and I like its optionals support and error handling approach. But I was put off by its noisy syntax, e.g. !?[]u8 to represent an error union of an optional pointer to a many-pointer of uint8. Also having to prepare and weave allocators throughout most of the code that needs to dynamically allocate (which is most of the code) gets in the way of the main logic. Even little things like string concatenation or formatting becomes a chore. Zig also doesn't have dynamic dispatch, which makes polymorphic code hard to write; you have to work around it through some form of duck typing. In the end I realized that Zig is not for me.
[1] https://github.com/khaledh/axiom [2] https://github.com/khaledh/axiom-zig