Zig looks really cool but it feels like it has a high chance of being a niche language. Rust never felt like that.
C++ has approximately eighteen different partially-overlapping categories of variable initialization, many of which are legacy-but-still-used! [0] And some of those categories have changed their boundaries every language version since C++11 (based on the definition of "aggregate").
C++ has three to five partially-overlapping kinds of type inference all in active use (auto, decltype(id), decltype((expr)), decltype(auto), template argument deduction)! This is interleaved with name lookup and overload resolution (below), so if some subexpression isn't compiling how you expect, you have a vast space of language features potentially to blame.
C++ has so many kinds of name lookup and namespacing that I'm not even sure how to count them. There's unqualified lookup, argument-dependent lookup, qualified lookup, class member access, etc. Sometimes you can't refer to things defined later (outside a class) and sometimes you can (inside a class). There is even undefined behavior if you mess up namespacing! (Undiagnosed ODR violations are every experienced C++ programmer's nightmare.)
C++ has ad-hoc overload resolution based on un-scoped identifiers, which even crosses namespace boundaries using one of the above name lookup modes. It has two kinds of user-defined implicit conversions ("explicit" and implicit) that also affect this selection process, on top of the zoo of "type promotions" inherited from C.
C++ classes have five to six kinds of "special member functions," some of which may be defined automatically by the compiler, each with its own rules for when and how, based on what else is defined in the class. These also contribute to overload resolution, of course.
[0]: https://blog.tartanllama.xyz/initialization-is-bonkers/
Rust has a complexity of its own, but it's quite different in scale and quality. There is exactly one way to initialize a variable, exactly one kind of type inference, only two ways for names to be resolved (directly or via an imported trait). Overloading, implicit conversions (of which there is only one kind, Deref), and the replacement for "special member functions" (Copy, Clone, Drop) are all based on exactly one mechanism (again, traits).
The article has a pretty accurate description of how people experience Rust's remaining C++-like complexity, IMO: "I don't remember the order in which methods are resolved during autoderefencing, or how module visibility works, or how the type system determines if one impl might overlap another or be an orphan."
But one important aspect it leaves out (not being a C++ article) is that if you mess up any of these, you just get a compiler error- and Rust is well-known for having extremely helpful error messages. In C++ you may get a compiler error (known for being extremely unhelpful) or you may get undefined behavior.
Zig is certainly a smaller language than either C++ or Rust, but that comes at a cost. I would much rather hear discussion of those actual trade-offs than yet another "Rust is just as complicated as C++" non-claim.
I have never seen anyone focusing so ruthlessly, so early, on nitty-gritty details of how to go about engineering a compiler and language that is and will let you be as close to optimal as possible in terms of compile-time and run-time performance. "Perfect software" as Andrew talks about.
In short, engineering choices taken early will let Zig be something that Rust maybe could approach too (in theory), but in practice never will. Of course Rust is something that Zig will never be, too.
If Zig can settle in the niche that C is used for today (including "nearby" areas where C programmers consider switching to a higher-level language), then it is already a great success. I think (rather: hope) what we will see in the future is that no single language will dominate certain fields anymore like it was the case in the 90's and early 00's.
Rust doesn't need to fit into every niche, and it would be harmful to bend Rust in a way that it fits everywhere. It would end up as a "kitchen-sink language" with tons of competing concepts and ideas. This is exactly what's currently killing C++.
You cannot "copy" simplicity into a complex language. (Not trying to start a discussions about whether or not Rust is complicated.)
What about extremely fast compilation? In theory a complex language could have a really fast compiler, but I don't think there are any historical examples of languages with a very slow compiler getting compilation down to around one second as the article describes.
https://dlang.org/blog/2019/07/15/ownership-and-borrowing-in...
I for one have a really hard time liking expressive ones, hence Go & Zig is my small but high-quality toolbox rather than a larger toolbox.
I just can't get that excited about the language itself beyond a certain point, I just want simple, predictable high-quality tools to help me produce simple, high-quality code and applications.
Anyway, all of the above is ofc subjective.
Good! Maybe you can actually get something done using it then, instead of getting lost in mastering lots of concepts just for the sake of it.
I do think that these sorts of language comparisons are useful, but they don't always generalize. Partially this is because what a language means to each person can vary. As long as they're understood in a very coarse grained way, I think they can still make sense, but it's tricky!
I'm one of those people, and, while I have yet to use Rust or Zig for any real work, at least so far I also see Rust as being more directly a competitor to C++, and Zig as the more direct competitor to C.
Or perhaps I should say analogue. Because, it's true, I might choose Rust over C. I'm even tentatively planning to, for one project that's still in the idea phase, and that I would normally have wanted to do in C. Though that's not really because I see Rust as being more C-like. It's more that I see choosing Rust as being perhaps more likely to be worth the extra effort than I've found to be the case for C++.
I did fairly large and complex program in Rust for bicycle computer, and, while I like developer ergonomic, speed, and memory usage, I'm disappointed by the total size of the binary, number of dependencies used, and compilation time.
I'm a C programmer. I haven't touched C++ since before C++11 became a thing, and I prefer C to C++. I'm much more excited by Rust.
Yes, you can turn many of these things off or ignore them and just use D as a better C, but the same could be said of C++ (for some definition of "better"). Or most languages, really, if you squint enough.
Just please do not make the mistake of believing that it is unique to Zig. Factor brings the best of Forth and Lisp together, so meta-programming or extending the language is possible quite easily, for example. You could extend the syntax or add constructs pretty easily, and so forth. Anyways, an example can be found here: https://rosettacode.org/wiki/Compile-time_calculation#Factor but this barely scratches the surface. It does not mention `<< ... >>` which evaluates some code at parse time. You can execute code before the words in a source file are compiled.
https://docs.factorcode.org/content/article-literals.html
https://docs.factorcode.org/content/article-syntax-literals....
https://docs.factorcode.org/content/article-syntax-immediate...
https://docs.factorcode.org/content/word-flags{,literals.htm...
I remember when I did something like:
SYMBOL: aligned-16-char
<<
: 16-byte-alignment ( c-type -- c-type )
16 >>align 16 >>align-first ;
char lookup-c-type clone 16-byte-alignment \ aligned-16-char typedef
>>
when I was working on some binding.You could use it in a struct like:
STRUCT: foo
{ bar aligned-16-char[16] } ;
Or something like this is pretty typical (when writing bindings/ffi): << "libotr" {
{ [ os windows? ] [ "libotr.dll" ] }
{ [ os macosx? ] [ "libotr.dylib" ] }
{ [ os unix? ] [ "libotr.so" ] }
} cond cdecl add-library >>
Those are just some examples, but it is pretty powerful. It supports (and encourages) interactive development. Profiling and debugging is a breeze and highly detailed and useful, you can easily disassemble words (functions), you can get a list of how many times malloc has been called in some circumstances, there is runtime code reloading (a vocabulary that implements automatic reloading of changed source files[1]), and so on. And on top of all this, you can compile your stuff to an executable that is less than 4 MB!And of course you do not have to do stack shuffling at all, you can easily use locals which is useful for math equations and whatnot. Plus did you know that the Factor compiler supports advanced compiler optimizations that take advantage of the type information it can glean from source code? The typed vocabulary (yes, it is not part of the language, but implemented as a vocab) provides syntax that allows words to provide checked type information about their inputs and outputs and improve the performance of compiled code.
I would like to repeat because if this was not the case, I would have never bothered with it: you can create a single executable file that is less than 4 MB of size if you wish so! Of course it encourages interactive development, but still, it is great to have an optimizing compiler that can do all this easily. And mind you, this part is also written in Factor itself and is available as a vocabulary (vocab).
[1] There is a vocabulary named io.monitors and loaded source files across all vocabulary roots are monitored for changes. You can read more about it here: https://docs.factorcode.org/content/article-vocabs.refresh.h...
---
So all in all, I think Factor is great. I was shocked at how modern (and how many) libraries it has, especially considering only a handful of people have been working on it. Slava Pestov created the language, and some people joined him later on. If you want to learn more about it, start here: https://concatenative.org/wiki/view/Factor. There are videos, there are papers, there are lots of resources to get started. :) The language misses a couple of things, but it is being worked on.
Also if comptime is simply compile time evaluation / execution, C++ has it with constexpr and templates.
Nim has this too I think.
Though judging by your comment not as powerful as Factor (I'm not familiar with it) extending syntax etc.
circle C++ probably gets closer: https://www.circle-lang.org/
What is unique to Zig is that it has these features without bringing together "the best of Forth and Lisp". Sometimes, just being pedestrian is a virtue.
There's also the downside that transpiling to C locks you into an ABI, which limits what compiler developers can do towards the end of the compilation passes.
Zig will eventually have this feature. It is a work-in-progress: https://github.com/ziglang/zig/blob/master/src/codegen/c.zig
It's tempting to view `comptime` as additional complexity, but the truth is that it replaces a much more complicated and hard-to-work-with system. That one is just one that people have gotten used to over decades.
> Most of this difference is not related to lifetimes. Rust has patterns, traits, dyn, modules, declarative macros, procedural macros, derive, associated types, annotations, cfg, cargo features, turbofish, autoderefencing, deref coercion etc
Nobody forces beginners to write macros. Beginners are only macros _users_. With time and experience, the need for macros emerges by itself, then learning them is a natural part of the process. But even then, nobody is forced to write any.
Dyn is also a very obvious concept to anybody who knows a bit of OO-programming in lower-level languages (e.g. C++). The choice to make dynamic dispatching explicit is arguable, but ultimately, although making it explicit is (AFAIK) Rust-specific, the concept itself isn't.
Complaining on pattern matching? C'mon :-) It's a bit like a Python programmer complaining that Golang has a switch/case.
I don't argue that Rust is hard or not, but it seems to me that the author was overwhelmed, and complained about everything, even simple things.
In my experience, in the Rust learning process (and programming experience), all the concepts above are dwarfed by the headaches induced by the borrow checker.
> Zig has it's own implementation of standard OS APIs which means that linking libc is completely optional. Among other things, this means that zig can generate very small binaries which might give it an edge for wasm where download/startup times matter a lot.
I'm curious about the details of this. Rust has `no_std`, however, it seems that in Zig, this is more (in a way) granular?
Macros in Rust aren't only confusing for beginners, because it has a completely different syntax (well, at least macro_rules! does; idk about proc macros) than the one you use for the rest of your program.
Even _if_ you draw the line between macro writers and users, you've effectively made macros a black box you're not supposed to look into. Having trouble debugging anything related to a custom derive or a macro? Too bad, macro's are hard. This is (a) not useful and (b) not necessary, as Zig clearly shows.
I think Rust made the C++ mistake of trying to accommodate everyone by having both low level control of things, but not too low level since that's dangerous, so we'll come up with some rules that you can't break, and oh by the way the rules aren't really ready yet, oh, and if you're not used to dereferencing pointers, don't worry we have this deref trait, what's a trait you say? and so on and so on.
It's effectively a barrier to entry, which, ironically, is a thing the Rust community tries really hard to combat.
If you mix a bunch of nice colors, blue, red, green, turqouise, purple, orange, eggshell white, you just get ... brown.
How? My impression is that they are not trying to do the language easier at every release. Contrarily, I see more new features added all the time (which is a good thing if Rust is your thing).
What I see is top-quality documentation. No doubt about it. Perhaps they focus on quality learning material, but the truth is the more I read, the more I scratch my head thinking "what is this construct and when and why do I need to use this?". Then overchoice[1] anxiety kicks in and I go back to zero, that is, my good old C.
I understand why it is this way but it is very much not ergonomic
Appeal to authority much?
It's not about how effortless the code is to write, but the total lifecycle effort. This includes for example maintenance, security, extending and on-boarding new team members.
I think one of the problems with this mindset is, you are only assuming the case with you have full control on the code-base e.g. writing thing from scratch so that you can only use those features in rust that you feel comfortable with, in other cases, you have little control on what others use.
I've read that as a (minor) complaint that Zig doesn't have Rust's pattern matching, not that there's anything wrong with pattern matching (and that Zig's switch still does 90% of what the author uses Rust's pattern matching for).
It's not necessarily bad. Do you prefer to put more effort into your program before it compiles, or debug it after it's up an running? Do you want a static guarantee, or do you trust yourself to get it right? These are fair trade-offs.
It strikes me that you start off meaning to rebut the author's point and end up supporting it.
I don't consider myself an expert in Rust, but hell, the borrow checker was always helpful. I.e. even if it prohibited a sound program, it explained its reasoning at length. It was easy to fix the issue even if it came.
Macros are meta programming, of course they are hard. And even then, there are ways around. Like cargo-expand, it makes procedural macros easier to reason about.
I know what a class and what an interface is, but Rust doesn't have these. It has a struct, which is kinda like a class without methods? It has a trait, which is kinda like an interface but with implementations for methods? It has ... implementations... which kinda make a struct to a class, but not really.
I don't mean to hate here, but these are all things that need to be understood in some kind of way.
I still feel Rust, Nim and Zig kind of get the right ideas, but they are not there just yet. I like rust with it's expliciteness and correctness, but I wish some stricter features were opt-in. I do not feel that the borrow checker is the ultimate solution to safety problems. Its strictness can make working with Rust a pain. As the author demonstrates, sometimes you know what you want to do and how to write it in other languages, but it can take quite a while to get it down in such a way that the rust compiler will accept it.
I think enabling a language to be garbage collected in general, while making a borrow checker opt in for special, time critical functions is the best of both worlds. I also think Rust may focus a bit too much on the terseness of its syntax. This can make modern Rust hard to read because there are so many special tokens.
I remain to be convinced that this is possible.
Rust's ownership model exerts huge design pressure on its standard library. There are some parts that just wouldn't work without ownership (like guards), and many that are far less ergonomic than they could be with GC (like iterators).
If you make a language GC by default with opt-in ownership, what does your standard library look like? It either isn't usable in ownership code, or it's crippled for GC code.
Check out D language with default GC and the ongoing effort opt-in ownership model:
https://dlang.org/blog/2019/07/15/ownership-and-borrowing-in...
I think the bigger problem is that GC pointers will have to work like Rust’s reference counted pointers do today, meaning you need to use mutexes, read-write locks, etc. to mutate anything behind them. Most shared-memory GC languages allow free data races on all member variables, and just provide unordered atomicity to prevent you from being able to cause a race that writes a bad pointer somewhere. Changing that would be a much stronger mismatch, and as long as that’s the case you’re still not going to be able to write Rust code like you would Java or C#.
It might also be a good idea to look at the standard library of Idris 2[1] — it has linear types and garbage collection, so it has many of the same issues you describe, and it also seems to solve them with duplication where necessary[2], at least sometimes.
[0]: https://github.com/rust-lang/rfcs/issues/414
[1]: https://github.com/idris-lang/Idris2/tree/master/libs/base
[2]: https://github.com/idris-lang/Idris2/blob/master/libs/base/D...
I could see the opposite working out very well. Having a GC type you can box other types in.
I'm not entirely sure I've heard anyone express this before. What do you like about it?
1. has a nicer user experience
2. gives a lot of confidence that a project will be reproducible across environments, with only a package.json file
Cargo is pretty close, but NPM is the gold standard for dependency management as far as I'm concerned.
I really like how there's a package for everything. Many other languages have adopted this method, but for example in Rust, most packages are not as mature as JS packages are. The JS ecosystem is responsible for spawning services that fund Open-Source developers. Packages are also easy to install. I spent a whole weekend trying to install Postgres and Drogon (a http server) on C++ with conan/vcpkg, and in the end I could only manage with a docker container installing these dependencies via apt-get, which was exactly what I did not want.
Furthermore, NPM is the package manager among package managers. No other package manager comes close. Python has too many options, virtual environments and so on. Rust's cargo is good enough, but I really do not enjoy having to install a seperate package (cargo-edit) just to add a package via the command line instead of editing text files. C++'s package management systems are most of the time a total letdown or don't have widespread adoption. Not only that, npm also takes care of a package maintainer needs (semver, transitive dependencies, etc)
---
Many people express dissatisfaction about build tools and the like, but as someone who got into webdev at the exact time people began building larger applications on the client, I love them. Sure, when they first appeared they were a pain to work with, but most modern build systems are amazing. I can just include most files (MD, SVG, images) and work with as if they were JSON/JS files and don't have to worry about how it's done internally.
---
Prototyping is really fast and important if you work with startups or want to create a proof of concept for a customer. I can throw together a functioning backend with http server and database in a couple of days.
Modern JS frameworks are uncomplicated and can be minimal if you know how to use them. For example, my personal site (https://juliankrieger.dev/) is written in Gatsby and React.js, but it weighs only 20kb. Now, there's not much on it but all content is rendered server side and only rehydrated when I need it. For anyone interested in getting even better results on a personal homepage, I recommend looking at 11ty for a static site generator and htm/preact for an absolutely minimal React implementation that only ships javascript where you really need it.
---
I also like how it enables me to write scripts for personal use and at the same time I can use Node for larger projects. I've used python for this in the past, but a large python code base can be a beast of its own.
Moreover, not having to recompile dependencies on a code change is a welcome feature. My main problem with Rust are the large compile times. Node even enabled me to hot reload code under the right conditions, making iterative development an insanely fast process.
Put everything you don't want to manage into Rcs, RefCells, Boxes and the like, and you'll more or less feel like you're using a (verbose) GCed language (with a loss of performance and safety as a natural consequence).
> I enjoy Rust and have been writing it for quite some time. However, I feel like server side languages still have a long way to go. Client-side languages in comparison have been only growing better
The fact you call them "server-side language" is a strong bias. Not everything is a client/server app.
It is an absolute joy to write code in D. Only downside is people complaining it has garbage collection.
The discord community operates like a cult (sound familiar, Rust community?) and any criticisms or anything not exuberantly positive results in a flame war.
I saw all of that happen several times so far with the community and it's ultimately what drove me away from the project altogether.
You will find me to be quite open minded about criticism but you're not going to get very far by misquoting me.
I have an entire kanban board[1] dedicated to improving safety, and "security and correctness" are both properties of the word "robust" which is the very first adjective ziglang.org uses to describe the language.
I could spend 20 more minutes on HN debunking the claims in this thread, but instead of rewarding your behavior I'm going to give that time instead to the people who have opened pull requests on Zig and help them get their code merged.
can you be a bit more specific?
Another one I personally brought up was that the standard library's utf-8 module had a decoder that panics on invalid input sequences, certainly setting consumers up for DOS attacks with malformed utf-8 inputs. EDIT: DOS vulnerability is still there (https://github.com/ziglang/zig/blob/master/lib/std/unicode.z..., PR to fix that was closed https://github.com/ziglang/zig/pull/4929). I never responded to the PR because it was at that moment I decided to abandon Zig altogether.
The response to the latter was pretty much "the standard library isn't meant to be used right now", to which I really don't have a response. There was a very, very long and heated argument in the discord channel about it where instead of addressing the concerns about DOS and security I was instead insulted for apparently trying to taint an otherwise perfect language.
The community is vile and the few examples I've seen of the maintainer disregarding safety and security in this way don't give me any amount of confidence in the project overall.
EDIT: Worth mentioning, the syntax and semantics surrounding Zig are not new ideas. I'm sure another project will pop up at some point to compete; many discussions I've seen in the language design channels on IRC and a few discord servers have many people arriving at similar conclusions Zig has made, without knowing Zig even exists. I think we're slowly converging on a language that looks a lot like Zig, but I don't think Zig will be its ultimate incarnation.
A response to a security concern should never be "fuck off".
The language is not yet production-ready for almost every production use-case, and that should not be a surprise to anybody that has looked into it a bit. We even had somebody make a "Using Zig in Production" talk that started with a few jokes on how he decided to do so despite Andrew publicly saying that it's too early.
Right now docs, tooling, the self-hosted compiler, making design decisions on corners of the language not yet finalized, getting more contributors, getting funding to speed up development (and give back to contributors), and building up the community are all needs with an immensely higher level of priority.
On the last point, the community, since that's my job, I'll spare a couple more words: "the" discord community doesn't exist. The Zig community is decentralized and anyone is free to start their own space, as stated in the Community wiki page of the project https://github.com/ziglang/zig/wiki/Community
So when it comes to Discord servers, at the moment of writing there are two listed in that document: the older, bigger one, and mine. You are probably talking about the bigger one, where I can see your discussions with other members. From what I can see in the logs, the discussions were calm and reasonable. I also don't see any of the insults that you refer to in your other comments. In case I missed them though, you'd need to raise the problem with the moderators of that space, and not chalk it up to 'the community' being a cult. This is very different compared to how Rust runs its communities btw.
I'm sorry, but from what I can gather in your case you simply had strong opinions on specific topics and other people just disagreed with you, partially for design (i.e. non strictly technical) reasons. From what I can see from your other comments in this thread, my only recommendation is to work on being more dispassionate when approaching a new community and when issuing PRs (btw a good way of avoiding doing useless work is to open an issue first or to find Andrew / other core contributors on IRC and get their opinion). At the end of the day Zig is an opinionated project where Andrew gives the final approval on what the language should or should not be. By missing that nuance, you built up expectations that in the end were unmet, resulting in understandable frustration.
That said, from my PoV, this doesn't justify excessive criticism of Zig and its community.
As for debating changes and raising criticism, we do that too, but to do that successfully you need to understand more the nuances in the history and design of Zig.
https://github.com/ziglang/zig/issues/6600 https://www.youtube.com/watch?v=880uR25pP5U
Therefore when setting priorities for language evolution it seems better to identify work that is less likely to result in breaking code, and prioritise safety over that.
I've been thinking about this recently: the borrow checker gets a lot of attention, but I think the majority of the learning curve of rust is actually due to "unforced errors" in the language UX which have nothing to do with the language's core USP's.
For instance, the module system just seems needlessly complex. Like you need a mod.rs file in each subdirectory, and the main.rs/lib.rs serves as the module file for the src directory, it took me like a day to figure out what exactly the rules are, and I can't understand why there can't be sensible defaults to define modules based on the file system structure alone.
> why there can't be sensible defaults to define modules based on the file system structure alone.
There could be, and in fact, I personally advocated for them. But there was significant community pushback; it turns out many people like to "comment out" entire modules when doing big refactorings. There are also some interesting edge cases, for example, you can use a #[path] attribute on a module to change what the path to the file is; without a mod statement, it's not clear where that would go.
With the mod.rs in place I can do this in my main.rs:
pub mod foo
and then in a different file do: use crate::foo::bar::*
But without the mod.rs it doesn't work and I don't know what the correct incantation is...Edit:
Nevermind, I thought this update meant you could remove the mod.rs completely but this is about declaring foo.rs as a file outside of the foo folder.
So you can't just do this like I was hoping:
.
├── lib.rs
└── foo/
└── bar.rsSo I'm not sure I'm a fan of this either. So now for a module with sub-modules, part of it is defined in the top-level directory, and part of it is defined in a sub-directory. Now if I want to move a module from one directory to another, I have to move two items in the file system.
> it turns out many people like to "comment out" entire modules when doing big refactorings.
It seems to me it would be much better to have an "exclude.rs" file or something to opt out of a sensible default rather than forcing extra work in the common case just to support the common case. Or else you could still allow a mod.rs if you want to be explicit about your module contents, and just assume it includes everything in the directory if it's missing
> There are also some interesting edge cases, for example, you can use a #[path] attribute on a module to change what the path to the file is; without a mod statement, it's not clear where that would go.
Again, I don't think a design should be optimized to support interesting edge cases. It should make the common case as simple as possible. If edge cases need to be supported, I'm sure a solution can be found
Also the simplicity in zig comes from a major innovation in controlled partial evaluation, which we've yet to really explore the consequences of. Rust had already made a major innovation in the form of the lifetime system. It needed to figure out all the implications of that so it made sense to draw from existing designs where possible for the rest of the language rather than piling on more new and untested ideas.
[edit: I'd like to clarify that the Rust team has tons of talent and good ideas and can probably find a goal more ambitious than this. Haskell has a definite aesthetic, and it fits some people/projects better than anything else. That aesthetic does not include simplicity.]
Rust has many syntax features that can be described as "Rust takes some common pattern that's onerous to write and read, and automatically infers it for you under certain conditions". On its face this seems like a strict improvement: you can still be explicit if you want/need to, or you can skip some boilerplate in certain cases.
But the problem comes when you're trying to learn about the language. Because these UX "optimizations" are layered on fairly arbitrarily - not unlike actual compiler optimizations - they sometimes create a very confusing landscape to try and form a mental model around, in the same way that compiler optimizations can make it hard to understand what will and won't make something faster.
This often plays out as:
1) You have some clean piece of code that does what you want
2) You add something innocuous to it
3) It no longer fits the sugar-pattern that the compiler was silently invoking underneath
4) You now have several sprawling errors because you're expected to be explicit about something you didn't have to be before
An inexperienced Rust programmer would (reasonably) assume those errors were caused by the thing that was added, and start trying to figure out what's wrong with it. But that's a red herring. The real issue, which is not indicated, is that a sugar-pattern was bailed out of.
I'm glad these shortcuts exist in some capacity: Rust is a complicated and fairly verbose language, and they make it less so. But I think they seriously damage the learning experience and early impressions that people form of the language. I've been using it for years and I still discover new quirks with this stuff that I didn't know about, and incorrect assumptions I had about the language itself that were driven by these mysterious mechanisms.
What if rustc had a "no-sugar" mode that people could use until they've gotten a handle on what's really going on? What if the language server somehow indicated inline when sugaring was being invoked?
Edit: A different way of phrasing the problem is that debugging is like navigating a landscape: there's a locality to it. "Did this change bring me closer to my goal, or further away from it? If closer, I'm probably on the right track, if further, probably the wrong one." Most languages mostly adhere to this idea of contiguous space-navigation. But these patterns in Rust are like constructing a maze across the landscape; there are cul-de-sacs and roundabout pathways you have to follow to get where you're going. There's also inconsistency about a given subject: you look at one piece of the terrain from a different angle, and it changes. It's hard to form a coherent, generalized mental map because base truth is relative based on what direction you're coming at it from. So over time instead of learning 2N concepts (lifetimes, references, iterators, boxes) you learn an NxN matrix of concepts (using references with boxes, using references with iterators, using lifetimes with iterators, etc...), because they interact with each other in unpredictable ways.
This may just deserve a whole blog post :)
Here's a mind dump:
> Zig manages to provide many of the same features with a single mechanism - compile-time execution of regular zig code. This comes will all kinds of pros and cons, but one large and important pro is that I already know how to write regular code so it's easy for me to just write down the thing that I want to happen.
This is a huge one for me, and I really don't understand why Rust didn't jump on this earlier. Using the programming language for configs, generics, macros, and anything else that you'd want at compile time just seems like such a huge win, instead of having weird preprocessors-like systems, some config file format with arbitrary limitations, and weird marcro-like systems that either have crazy syntax (like `macro_rules!` in Rust), or that are way too limiting (like `const` functions in Rust).
Jon Blows language seem to take a similar stance as Zig; let's see if it ever hits public beta.
> On the other hand, we can't type-check zig libraries which contain generics. We can only type-check specific uses of those libraries.
This is definitely a concern I have about this "C++-like generics". My experience with C++ suggests that this is bad, but on the other hand, improving even just error messages would be such a day-night improvement, that I don't really trust my judgement on this one.
> Both languages will insert implicit casts between primitive types whenever it is safe to do so, and require explicit casts otherwise.
Is this really true? I seem to recall having to have plenty of `as usize` in my code when using smaller integer types for indices, but maybe this has changed (or maybe I'm misreading what's being said here).
> In rust the Send/Sync traits flag types which are safe to move/share across threads. In the absence of unsafe code it should be impossible to cause data races.
This is probably Rust's main selling point, because as far as I can tell, no other mainstream language comes even close to getting static thread safe guarantees (up to your definition of mainstream). As time goes on, however, I'm getting less and less excited about this, because most of my programs are not multithreaded, and the very few times that I need multiple threads, there is often very obvious and small boundaries in between the threads. It's just not very interesting to me that I _could_ be writing programs with thousands of threads all jumping around without having to worry about data races, because I don't really worry about it in the first place. But still, I as a Rust programmer, have to pay the price for this option being available.
> Undefined behavior in rust is defined here. It's worth noting that breaking the aliasing rules in unsafe rust can cause undefined behavior but these rules are not yet well-defined.
I'm not sure what to say about this, except that it's surprising that there seem to be a lack of voices about this in the Rust community. How can anyone comfortably write `unsafe` code without knowing what the rules are? Especially when the compiler is so "good" at depending on the "rules"? I don't understand. I have pretty limited experience with unsafe, but I have written some, and was often confused about which bugs were my logic bugs and which were the compiler assuming I didn't break some rule I didn't know about. Combine this with a poor debugging story overall, and you have a pretty miserable experience programming.
Maybe this isn't a problem in practice, or maybe all people succesfully writing `unsafe` code for libraries are also `rustc` veterans?
> @import takes a path to a file and turns the whole file into a struct. So modules are just structs.
This is a very nice approach! I remember from earlier Rust that the module system was a real pain point for beginners, and can also remember really struggeling with it. Curiously though, I also remember looking back, not understanding why anything was confusing about it. This was also redone(?) at some point, and I think it's nicer now.
> In rust my code is littered with use Expr::* and I'm careful to avoid name collisions between different enums that I might want to import in the same functions. In zig I just use anonymous literals everywhere and don't worry about it.
I've always been bothered by Rust's inability to infer the `enum` type in a `match`; in other places Rust has no problems being automagick, and this is really very annoying to go around, either with `use Foo::*` before each match, or having it in file scope and hope for no collisions. Zig seems to take exactly the approach I'd go for.
> Re allocators
I think Zig's stand on explicit allocators is very good; I've seen enough bad code in other languages that allocates here and there for things that, very clearly, doesn't need to be there. Having the language be explicit about allocations makes it easier to stop and say "hey wait a minute, is this realy the way I'm supposed to do it?", but without having to jump through hoops if you _just_ want to allocate something somewhere (define a global allocator yourself). And, as a bonus, it's easier to handle the allocations of other peoples code.
> Zig has no syntax for closures.
I definitely need to write more Zig to find out whether this is a problem or not. I've written a lot of C++ lately, and while there _are_ closures available, I think I've only used them once. Maybe the reason for my comparatively heavy closure usage in Rust was that so many methods in the standard libray took closures that you're shephearded into making similar methods for your own types.
> Zig's error handling model is similar to rust's, but it's errors are an open union type rather than a regular union type like rust's.
I really think the error story is why I prefer Zig to Rust now. The giant error `enum` in Rust is definitely what I'd go with because it's simply not feasible to manually track which functions return what errors and making individual enums yourself, even though this is super easy for the compiler to do, like Zig shows.
Not to pick on anyone in particular, but sometimes it feels like many programmers think that a program only consist of the happy path and that errors are somehow rare and not worth dealing with properly. Both Rust and Zig are huge helps to combat this mindset, but I do think that Zig comes out ahead, simply by being less annoying to work with. Also, while some people might say that `Result` just being a part of `core` and not a magic special language thing is cool, I do appreciate Zig's usage of `?` since it's way less typing for something that happens _all_ the time.
> Zig's compilation is lazy. Only code which is actually reachable needs to typecheck. So if you run zig test --test-filter the_one_test_i_care_about_right_now then only the code used for that test needs to typecheck.
I didn't know this, but this is awesome!
> Zig has absurdly good support for cross-compiling.
I've never understood why cross-compiling isn't an out-of-the-box feature in all languages. Don't you basically just have to target a different instruction set? Well, and a different executable format. But still, compared to all of the other crazy things compilers are doing, this seems very straight forward in comparison.
> Zig has an experimental build system where the build graph is assembled by zig code.
See above. I really really really don't understand why all languages doesn't do this already.
> In rust, blocks are expressions.
This is something I really like about Rust and a pattern I've used a lot, where I'd say
let some_thing = {
let foo = ...
let bar = foo.baz() + quiz();
...
foo
};
to avoid accidently using `bar` somewhere else. Granted, since Rust allows shadowing this isn't really
a problem most of the time, but it's definitely something I miss when writing C++.
Zigs version is somewhat verbose, but I'll manage.> There is an in-progress incremental debug compiler for zig that aims for sub-second compile times for large projects. Based on progress so far, this is a plausible goal.
Andrew's work on binary patching executables is really cool. I hope we'll get to compile times this low, even for moderately sized projects.
> real 23m27.475s
This is just sad. Despite all the work the contributors to `rustc` are doing, it just seems that they are in a completely different league with respect to compile times than what I'd like. I hope the steady progess they're making will either make some jumps, or continue for a while :)
> Main points so far:
This is a great summary, and I think people reading it (or the whole post) will have a pretty good idea of where they stand re. the two languages.
Just a few small things:
> This is a huge one for me, and I really don't understand why Rust didn't jump on this earlier.
Doing this well is not easy. Exposing a full language at compile time isn't a difficult feature, but doing compile-time execution in a sound, safe way is not simple. For example, cross compilation becomes more of a thing. It is easy to accidentally break the type system. I don't actually know how Zig implements comptime, but I expect given Andrew's chops, it's probably in a good way.
(And, one can argue that there are pros and cons here too, the article gets into them a bit. Doing everything this way has significant drawbacks, as well as significant pros.)
> I'm not sure what to say about this, except that it's surprising that there seem to be a lack of voices about this in the Rust community. How can anyone comfortably write `unsafe` code without knowing what the rules are? Especially when the compiler is so "good" at depending on the "rules"? I don't understand. I have pretty limited experience with unsafe, but I have written some, and was often confused about which bugs were my logic bugs and which were the compiler assuming I didn't break some rule I didn't know about. Combine this with a poor debugging story overall, and you have a pretty miserable experience programming.
There's just not a lot to say. The exact details are being worked on. This takes time. The team isn't going to make rules that invalidate large swaths of existing code, and has a history of making compatibility warnings with a long time before things break.
There is a fairly clear list of "this is absolutely not acceptable," and a bunch of "it depends...".
It's also just that the vast majority of people never need to reach for unsafe in the first place, so it isn't a huge pressure on a lot of people.
Why? I don't think I've ever seen a concrete example of why this isn't simple (but I'm not really a compiler person, so there might be!). I can imagine plenty of ways of doing Bad Things, like adding compiler flags based on what day it is, but I can't immediately see why this would _break_ anything. What do you mean by breaking the type system? Accidentally getting non-typechecked code in the compiler, or running into problems with the compiler thinking two equal types are distinct?
> There's just not a lot to say. The exact details are being worked on. This takes time.
I understand and appreciate this! Maybe I should've phrased myself better: I don't understand how people are using `unsafe` today successfully when something as fundamental to the Rust language model such as aliasing isn't really defined properly yet. It sound like people writing books without the rules on verb conjugation being really set. It sounds to me like plenty of the unsafe code out there might end up breaking at some point, and that, by extension, all other safe code out there is basically built on extremely shaky grounds.
I might be overreacting here though, since I don't write a whole lot of Rust anymore and am pretty distanced from the community. Considering their track record, I'm sure it'll turn out just fine.
---
> (By the way, Hacker News doesn't support markdown;
Ah! It's always tricky to remember which places support what syntax with these things. Thanks!
Does this also apply to the in-language build system? Given both Rust and Zig's ergonomics, it just seems so brilliantly simple (at least in hindsight) to let the build system be a library. Is it just coincidence that I've only heard of this approach for zig and Jonathan Blow's language, or is there a technical reason this is more difficult than it seems?
> This is a huge one for me, and I really don't understand why Rust didn't jump on this earlier.
I believe this outcome was mostly defined by the history. Here is my reasoning:
Rust, at least since 0.5, was undoubtly designed as a replacement for C++ (of course that doesn't necessarily mean that it only appeals to C++ programmers), and C++ was notable for its unexpected sophiscation and problems with its primary compile-time mechanism, templates in the other words.
Rust replaced templates with two other features: traits and reimagined syntactic macros. The former formalizes C++'s long-waited concepts feature and avoids issues with C++'s "ad hoc" polymorphism, while the latter deals with code generation, the remaining use of templates.
Traits (and lifetimes) required a complex type system, which takes time to compute and ideally the result for some file should be intact when other files have been changed. This constraint makes most additional compile-time mechanisms undesirable for addition because they can create unexpected dependencies between files, so recompiling one can trigger others. Note that the compile-time code is still a code, with states and everything attached, so this is far from trivial. (What if your compile-time code needs an external input?)
Zig and Nim show what the different starting point might result in: they didn't have to solve problems with C++ templates (among others), and could retain ad hoc polymorphism. This limits an ability to incrementally compile, and what's common with those languages? Much simpler type system, which compiles fast and makes the incremental compilation less concern for them. Rust needed a complex type system for its goals, which unfortunately limited its options for compile-time mechanisms.
Where is this "complex type system -> long compilation times" meme comes from?
Most of rustc's time is spent in llvm. And bottlenecks are identified as monomorphization, producing large amount of LLVM IR and lack of binary dependencies.
Type checking is a small portion of time, and not a bottleneck, IIRC.
The problem with this is that it's incompatible with a well-designed compiled[0] programming language; a cross compiler can't replicate the architecture-specific behaviour of the compiled code because the target architecture is unavailable or possibly even nonexistent at the place and time the compiler runs.
Consider, eg, a compiler on ARM targeting x86, when the code uses x86-specific assembly or intrinsics, or more subtly depends on x86-specific handling of things like pointers or integer overflow. If you instead compile the compile-time code for ARM, you a: need two different codegens (soon to become three when you compile the compiler to run on RISC-V), and b: now the compile-time code is depending on ARM-specific semantics, which is even worse.
Giving up on cross compilers means your language isn't well-designed. Giving up on architecture-specific behaviour means your language isn't designed for (direct[0]) compilation. (The latter is a legitimate choice, of course, but its negation is also a legitimate choice, and thus a legitimate reason not to support same-language metaprogramming.)
0: in the sense of C-like compiling directly to equivalent machine code; obviously any language can be compiled in the more general sense of producing a working executable
I mean, it can't possibly be nonexistent when you write the program, since if you don't have any idea what the target looks like, how can you output code for it? Do you mean physically existent?
I think I see the overall point though, namely that arch specific code would be a mess, with which I agree. But isn't this okay? I mean, if you really want to switch on some platform specific thing at compile time, then different behavior for different host platforms is exactly what you want.
To me, there's basically two uses of compile time execution: precomputation and codegen. If we look at the following snippet
const std = @import("std");
pub fn main() anyerror!void {
std.debug.warn("{}\n", .{.{
@sizeOf(*usize),
comptime @sizeOf(*usize)
}});
}
we're looking at the size of a `usize`, which is pointer-sized, and we're taking it both with and without `comptime`.
If I compile and run this on my system, it prints out 8 and 8, since I'm on a 64-bit system. /h/m/t/zig$ zig build run
struct:5:21{ .0 = 8, .1 = 8 }
However, Zig does support cross compilation, and I can compile this for 32-bit mode, and run it, which then gives me 4 and 4. /h/m/t/zig$ zig build run -Dtarget=i386-linux-musl
struct:5:21{ .0 = 4, .1 = 4 }
It's worth noting that the docs for `@sizeOf` says "This function returns the number of bytes it takes to store T in memory. The result is a target-specific compile time constant.".> you a: need two different codegens
Won't you need `n` different codegens if you support cross compilation to `n` different targets anyways?
One thing that feels missing from Zig though is encapsulation. I don't believe that you can declare struct fields private, such that they can only be accessed by methods. This seems really important to me, not only as a technical means of enforcing invariants, or hiding internal-only implementation details that may change, but even just as a communication of intent. There is a big difference between "you are invited to read/write this directly" and "please don't read/write this."
C doesn't have this, true, but at least in C you can put internal-only structs in a .c file instead of .h, making the members effectively invisible to clients. Does Zig have anything comparable?
I used to be suspicious of the Python approach, because it doesn't even pretend it's enforced by anything but the honor system. But I've discovered that, in practice, my Python-using colleagues are no less likely to respect `_field` than my Java-using colleagues are to respect `private field`.
Naming conventions seem like the way to go.
Give that Zig is a low-level language where "unsafe escape hatches" are the norm, it wouldn't surprise me if this ended up being an optional compile-time check rather than a mandatory one (as a sibling comment suggested).
I think Rust always requires explicit casts. It's a bit annoying tbh - especially for array indexing - indexing with a u8, u16 or u32 should always be fine but you still have to do `as usize`.
Edit: But as the reply below says, please don't.
Much better to use ::from() or into() for numeric type conversions, which are only implemented for types that are guaranteed to fit the value, unless you know that truncation is perfectly fine behavior in a particular instance... which it rarely actually is.
I've had a few small side projects I wanted to do which involved exposing an API.
Http standards are evolving and soon the implementation will get stale, being in the standard library there will be need to updated it and possibly maintain backwards compatibility, even when it becomes clear that a new design may be a better option. So, suddenly people will start using new, often better, libraries but you need to keep find workforce to maintain the old stdlib http library.
That is mostly why I keep thinking that it would be better to have it as a separate library, with an easy-to-use package manager to discover and use it.
Why would it "get stale"? It doesn't get stale in Golang.
If anything, being part of the standard library is a greater assurance for more eyes going into it, and not having it get stale, as opposed to the language having 5-6 half-abandoned third party libs...
The real future though is theorem proving systems, and generating code from them. I don't mean so much Agda, Idris, etc., which try to approach theorem proving from a programmer's point of view. But theorem proving systems that embrace the full spectrum of abstract and correct thought.
Zig and Rust try to get there without paying the heavy price that theorem proving incurs (Rust pays that price more than Zig already, though). But to truly progress we need to pay that price and make it lower and lower with time.
I've heard the same thing about model-driven development 25 years ago. Outside of some very small niches, it's pretty much dead now.
> But theorem proving systems that embrace the full spectrum of abstract and correct thought.
And here's where reality and the ideal world collide: the vast majority of applications consists of incomplete models (in that data and/or understanding of the problem are incomplete), quickly shifting goals, and pressure to release.
Theorem proving systems offer nothing that helps with this class of software - abstract and correct thought sounds marvellous in an academic environment or if you have unlimited budget to hire top talent and perform thorough analysis.
In practice, however, your typical line-of-business application is faster, cheaper, and well-enough programmed using traditional languages and a healthy dose of best-practises.
Optimisation and platform support are another area where a TPS won't be very useful. Every hardware has its quirks and workarounds are required to get the best performance or avoid pitfalls. These aren't easily expressed (and identified) using abstract thought and models.
Last but not least, no sane company is going to just throw away decades worth of investment in applications, libraries, and (software-)infrastructure just for a nebulous promise of what basically boils down to smarter and better staff with the right tools.
Theorem proving systems have their place and that place might get bigger in the future, but they're most certainly not the panacea of all software development.
There is room for "experimental" programming of course, where you experiment with stuff and you are glad that you get it somehow working in the first place. But should stuff that peoples lives depend on depend on experimental software like that? No. And as software more and more becomes part of our lives, I don't see much software for which that attribute does not hold.
Same goes for hardware.
As a mathematician (and programmer on the side) who regularly works in Coq, my impression is that Rust does represent "the future of programming" (or rather, my ideal of it). Type systems are the only mechanism (that I know of) for formally ensuring properties of programs, and proof assistants and languages like Rust lie on two extremes of the spectrum. The former puts the type system in focus; indeed all my work in Coq is about convincing the compiler that certain functions (terms) type-check. The latter puts types in the background, trying to prove as much as possible with minimal friction.
What's the alternative?
... now. The goal is to have full protection against UAF (in safe-mode only, of course).
Then it seems to me that Rust still has an advantage, in that it offers full protection of UAF with zero runtime overhead.
Of course, I too believe statically verifying is important for mission critical software. But it comes with a cognitive overhead.
For rust I had to add some custom linker flags, use global_asm for the startup code, and then with no_std I could just call my own main function. The only really annoying part of the whole thing is that there is no feature in Rust to force a function to not be removed. I could override the global allocator to make allocations very fast.
For C/C++, it's the same as in Rust, except you can use __attribute__((used)) to make a function not get removed. It's also easy to override memory- and string-functions if you have system calls that do these things faster. Overall the C/C++ environment was the fastest.
For Nim, I only had to use C++ as a backend, and then call NimMain(), add a few extra flags, and it would just work. I only wish that Nim was more popular.
comptime{
asm("nop"); //Your asm here
}
Calling Main From an arbitrary point: const root = @import("root");
//....
root.main();Can we use `pub` to make it public and not to be removed ?
(Not at op, who does write tests:) You are writing tests, right? ;)
Things like browsers and operating systems are all heavily tested and fuzzed using tools like asan. They still have security issues from UAF.
That only covers gl/sdl but I think gtk worked with that method too.
I really want to use it more, but I cannot get past the syntax.
For example, method chaining:
std.fs.cwd().openFile("does_not_exist/foo.txt", .{})
The aesthetic of a language is really important to me, and method chaining that allows for lines like this somehow doesn't feel right.> One of the key differences between zig and rust is that when writing a generic function, rust will prove that the function is type-safe for every possible value of the generic parameters. Zig will prove that the function is type-safe only for each parameter that you actually call the function with. On the one hand, this allows zig to make use of arbitrary compile-time logic [...]
This is fundamentally identical to how C++ templates, constexpr and concepts work. Its a really flexible system (you can implement how you want to type check things using constexpr), but has three cons that Rust system does not have:
- can't typecheck library APIs, so library authors aren't sure if their "constraints" are correct. Testing this requires writing lots and lots of compile-time tests.
- errors deep inside a library implementation when user code passes incorrect arguments to generic APIs.
- rust traits can be used for static dispatch, or boxed and used for dynamic dispatch, C++ at least can't really do this well.
It would be cool if someone could explain how Zig fixes or improves upon these problems that this system has in C++. C++ tried to fix this with concepts, but failed.
This was a nice read that has motivated me to learn Zig. I want to know how Zig improves on these C++ issues.
This is the most relevant part to me. As someone who will probably never write a line of either myself, the way I will work with languages like this is thorough libraries or extensions of higher-level languages like python or ruby. To that end, safety is the most important factor to me, with performance very much second. While rust has unsafe operations, these are relatively easy to audit if the code is open source. Ok, so to the programmer, Zig may be more ergonomic than C or Rust. But until Zig can offer the safety assurances of Rust, I'm still rooting for Rust to take over the world as the dominant and de-facto low-level language.
Exactly! That's a feature. Rust is a step forward to a future where code is based on sound theory (type Theory) not on some ad-hoc "seems to be working" basis.