Assorted Thoughts on Zig and Rust
scattered-thoughts.net
scattered-thoughts.net
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.
So 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
(Everyone seems to love and/or hate the module system for various, opposing reasons. It is a great mystery to me. Explaining the module system is like, kind of my white whale.)
I just wonder how much of that is as-hoc reasoning, and it doesn’t seem like it’s a particularly good sign.
My working theory: each language does modules in a different way. People think "Oh, a module system, I know this" and then run into issues when it works differently than their language. Just a theory though, I have not been able to validate it, or to figure out an explanation that works for all people.
And yes, more generally: as Rust continues to grow, more and more people will hit more and more edge cases, and they cannot always be fixed, thanks to being backwards compatible. That's just how things are as they grow up. More and more users also brings more and more use cases. It's a feedback loop.
Now I'm more interested in what the complaints about it are, and specifically what they're in comparison to. Either they're not used to using so many modules in a method such as this, or they're honestly expecting some better system for their use case that I'm unaware of, or a little of both, so my curiosity is piqued.
I think the same can be said for match statements on enums: in Rust you need to match against the fully qualified type name, when in other languages (including Zig) you can infer everything up to the enum case, since it's already specified by the type you are matching against.
These kinds of things just seem weirdly inconsistent, since in many cases Rust favors inference and elision to remove boilerplate, while in other cases it requires explicitness when there is no technical reason for it to be required, and other languages handle inference just fine.
People expecting to put "mod foo;" at the top of foo.rs, to declare that foo.rs is a module.
People expecting to put "mod foo { }" with the contents of the file inside the {}s to declare that foo.rs is a module.
People expecting that "use" declares a module, not "mod."
People expecting every file inside a directory "foo" to be the contents of the "foo" module, regardless of filename, all concatenated together.
People expecting that mod statements never need to exist because it should be inferred from the filesystem.
General confusion about the privacy rules; that "pub" may not mean that your thing is globally public.
General confusion about crates vs modules; main.rs and lib.rs being in the same directory, but declaring different crates. Not understanding how to import stuff from lib.rs into main.rs, because it feels a bit different than other modules.
People used to also struggle a lot with "use" and paths pre-Rust 2018, but that's been mostly cleaned up at this point.
I would love to see this. The issue isn't that people hate the idea. The issue is not breaking existing projects and workflows when making such a change. If we could find a way to make this work without breaking existing projects and workflows, I think there's support for doing so.
Code written for the 2015 edition should generally just work with the 2018 edition module system. That's part of what we worked to ensure, and that's why we didn't mix in changes that might have reduced that compatibility.
If there is such an easy to define 1:1 mapping, then could there be a rustmods-on-save tool that automatically adds mod statements according to a fixed scheme whenever any *.rs file is created in the current directory or a child directory?
That'd also get us halfway towards just making it automatically work without `mod` statements.
I think the main reason why the foo.rs foo/bar.rs stuff was done was so that the tabs in an editor have informative headings. Instead of ten times mod.rs you have foo.rs, baz.rs, test.rs and so on. Personally I don't like it either (for the same reason as you) but the old way is still possible, even on the 2018 edition, so that's what I use.
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.rs foo/mod.rs
you can now use foo.rs
that's it. Contents of the file are exactly the same.Nothing to do with changing the mod line, or the use line. Those all stay the same. It's about files in the filesystem.
mod foo {
mod bar;
} .
├── lib.rs
└── foo/
└── mod.rs
└── one.rs
└── two.rs
└── three.rs
└── four.rs
└── bar/
└── mod.rs
└── one.rs
└── two.rs
└── three.rs
└── four.rs
Which feels to me like a very common folder structure in other languages.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 :)
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.
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.
EDIT: In fairness, Zig's presentation is pretty likeable. You can do a comptime expression pretty trivially in LISP, a comptime parameter or block would require actual effort.
https://ziglang.org/#Order-independent-top-level-declaration...
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/
Could you elaborate on this?
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.
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've seen this play out a number of times. "Rust cannot compete with C, because C is too entrenched in embedded." "I do embedded development in Rust, so it can." "Well, I work with these chips that Rust can't target yet, so it can't, for me." None of these statements are incorrect, but they can lead to huge back-and-forths.
Words are hard.
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.
Have you researched this space already[1]? By default Rust doesn't optimize for the resulting binary size, but there are lots of things that can be done to bring size down where you'd expect.
> number of dependencies used
When this comes up it becomes as much a technical discussion as a philosophical one :)
> and compilation time.
No arguments there. There are some things that can be done in your project to avoid spending too much time (simplify bounds to minimize recalculation in the type system, avoid proc macros, leverage cfg conditional compilation), but they are work arounds.
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.
I use D as better C. I use it as better C++ at times. Once ownership/borrowing [1] is stable, I'll use it as a better Rust too.
For close-to-the-metal code, I use D's inline assembler [2]. "To go any lower level than that, you'd need a miniature soldering iron and a very, very steady hand." (Andrei Alexandrescu)
[1] https://dlang.org/blog/2019/07/15/ownership-and-borrowing-in...
"One of the earliest design decisions that Walter made about D was that it would be easy to use with software written in C. Many widely-used libraries are implemented in C or have a C interface. He wanted to provide an easy path for established software companies to adopt the D language. A straightforward approach was to guarantee that users of D could immediately take advantage of any C library their project required without the need to reimplement it in D." from "Origins of the D programming language" by W.Bright, A.Alexandrescu, M.Parker.
D has an optional conservative mark-sweep Boehm GC which only gets triggered when you attempt to allocate something on the heap. If you don't do that, it won't bother you. Finally, you can explicitly disable it.
People generally ignore or miss the fact that D really shines when it comes CTFE, reflection and metaprogramming.
You still get the following language features, which is more than what C, C++ or Rust have to offer (though Rust is hot on D's trail).
Unrestricted use of compile-time features
Full metaprogramming facilities
Nested functions, nested structs, delegates and lambdas
Member functions, constructors, destructors, operating overloading, etc.
The full module system
Array slicing, and array bounds checking
RAII (yes, it can work without exceptions)
scope(exit)
Memory safety protections
Interfacing with C++
COM classes and C++ classes
assert failures are directed to the C runtime library
switch with strings
final switch
unittest
printf format validation
https://dlang.org/spec/betterc.htmlMemory management is one of the more important aspects of systems programming so it is useful to be clear about what is the main strategy used by each language.
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.
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`.
There are lots of footguns in JS, but I've never seen this cause issues.
That's a very interesting observation. You really have to go out of your way to access private fields in Java -- I can only recall about 3-5 instances of doing that in my 15+ years programming in Java. You have to look up a field by name, and call setAccessbile(true) on its reflection... and this is definitely going to raise eyebrows in code review. Some of the DI frameworks do it as a matter of course, but that's kind of a different thing...
(Now, package-private is a different matter. In that case you can[0], just put a class in the same package and access from there. I've done that... twice or so?)
What is your recollection in terms of doing the same thing in Python? Obviously, this is just going to be anecdata, but it could be kind of interesting.
[0] Modules change this a bit, but it doesn't seem modules are really a thing outside the standard library yet. (I'm programming in Scala these days, so haven't kept up with Java practices.)
FWIW, Java is also where I see stringly typed designs, too. I'm increasingly coming to fear that languages with more safety-oriented features aren't associated with safer practices because those features encourage safer design, so much as because they tend not to attract programmers with a swashbuckling attitude in the first place.
Definitely agree about "stringly" typed programming becoming an increasing issue in Java over the years, but that had more to do with over-use of instanceOf, etc., not so much actual strings (as in className).
> I'm increasingly coming to fear that languages with more safety-oriented features aren't associated with safer practices because those features encourage safer design, so much as because they tend not to attract programmers with a swashbuckling attitude in the first place.
For the life of me I cannot parse this sentence. Could you please rephrase or expound? Is there a missing negative somewhere, or...?
> I'm increasingly coming to fear that languages with more safety-oriented features are[] associated with safer practices [not] because those features encourage safer design, [but] because they tend not to attract programmers with a swashbuckling attitude in the first place.
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).
Naming conventions seem like the way to go.
Zig looks really cool but it feels like it has a high chance of being a niche language. Rust never felt like that.
You cannot "copy" simplicity into a complex language. (Not trying to start a discussions about whether or not Rust is complicated.)
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++.
That niche is "code that needs to be portable to any system with a C compiler", or "... to a particular system that only has a C compiler" so i am not sure it can.
There are lots of projects using C that aren't in that niche, but i don't believe there is a good reason for any of them to be using C over C++ these days. It's either history, inertia, or Luddism.
That's the old (and frankly: tiresome) mindset that C++ is a successor and improvement of C. After using "modern C" (as in C99 or later) for a while it becomes quite obvious that this isn't the case anymore, instead C++ was a fork of C and developed into a very different direction (including developing the original C subset into a non-standard C dialect). Especially with more recent C++ standards, C and C++ have become different languages with very different goals.
It really isn't. It's based on comparing the languages as they exist today. C++ is vastly more productive.
We're not using string based macros, but hygenic macros.
We're not using goto error, but instead RAII.
We're not guessing if a function returns 0 or 1 on success and if the big struct we passed in as a pointer is valid on either of those, we're using an ADT that makes it clear.
Slices mean everyone aren't reimplementing 1000 morphs of the same buffer struct.
And the deeper separation between code and data that the fat pointers give you and how the more you go down OO principles, the less idiomatic it feels, really feels more like a giant C codebase than a C++ one to me.
In C one has to be very disciplined about this type of stuff and needs to put much more thought into module API design (for instance to enforce memory management rules), because the language itself is extremely "freestyle".
You would think that, but for example I think that, for example, Oxide should definitely have picked zig, if it were more ready. Obviously it's not, and oxide wants to ship now.
https://dlang.org/blog/2019/07/15/ownership-and-borrowing-in...
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.
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.
I have seen many say just combine PL design innovation and tooling part and it will be perfect but I think this will not happen because it becomes very difficult and sensibilities of these approaches do not match.
It is just innovation mostly guided by practical engineering concerns. In this case: How to avoid requiring expensive heap allocation for async tasks, and how to be sure you don't crash from a stack overflow due to unexpected input for example.
I suspect that the excellent Zig comptime support is made possible by intentionally going for a pretty unambitious type system. Otherwise how could you soundly let arbitrary code generate a type? A fancy type system that wants to reason about that will fail, or at least cause the whole language to revolve around making that work.
I think you are right that it would be very hard for a language to innovate both here, and in the traditional PL-teoretical way.
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.
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.
It's definitely a matter of personal taste, but to me, Rust seems a monumental, awe-inspiring shrine erected to worship at the altar of accidental complexity. I admire the technical achievement -- I never imagined accidental complexity could be given such spectacular prominence -- but having spent my share of time with both C++ and Ada, I'd like to look elsewhere. I don't know if Zig will do the job, but the vision it has for low-level development is so refreshing, radical and different from everything else I've seen in my >20 years of professional software development that I'd like to give it a chance before settling for an improved C++. Anyway, it's a matter of personal aesthetic preference.
I'm also not trying to convince you to drop Zig and settle for Rust! You can use either or both or neither, I don't mind! Rather, my point is that Rust's direction relative to C++ is the same one you praise Zig for- it provides the same control with drastically more economical application of fewer language features. Just because Zig goes further (again, at great cost to things like tooling, error checking, and messages) doesn't mean Rust didn't make a lot of progress.
BTW, I haven't "adopted" Zig that I would need to drop it for anything (it's not even 1.0 yet). I'm still with C++ for the time being. But I'm deeply impressed by Zig's revolutionary design and complete rethinking of low-level programming that I'm keeping a watchful and hopeful eye on it.
But this makes me suspect we may be using very different definitions of "accidental complexity" here: Your usage seems to apply to programs, which wind up over-specifying low-level details in both languages. My usage of the term applies instead to the languages themselves, and the level of extra pain they inflict on programmers who have already accepted the C++/Rust/etc aesthetic.
C++ is highly backwards compatible to C to the degree that it's almost (but not quite) a superset. Of course it's quite easy to start creating cpp files. Also C++'s benefits were quickly realizable while for Rust's safety benefits to play out, you need to have replaced significant portions of highly risky components (e.g. those that parse user data, have a history of bugs, etc). Adding Rust to an existing C++ codebase is much harder than adding C++ to an existing C codebase.
Also do you have a link for your claim? I'm interested in reading on the early rise of C++.
so, uh, how does Rust define behaviour if you export a same-name symbol from two rust dlls and load it from some executable ?
The only way to get matching names is to ask for them explicitly, via FFI.
but, after checking apparently this adds a hash of the function to the name mangling - how does that work when you want to call it from another language ? e.g. for instance you can call C++ code directly through some dialects of Lisp, Perl , ADA, or D (AFAIR) as they all have libs or mechanisms that kinda understand C++ name mangling - how are you going to do the same with, from what I'm seeing, "name_of_the_file::name_of_the_function::some_hash" ?
> The only way to get matching names is to ask for them explicitly, via FFI.
and what happens if you have two libraries which expose the same extern-C function name ?
If you do wind up exporting the same name twice, you just get a linker error, because Rust doesn't play the same games C++ does with linkage. (This is also true of C++ FFI- the problematic ODR-violation stuff tends to involve more complex language features than `extern "C"`.)
this does not answer the question of whether the behaviour is defined if multiple libraries export the same name (which is the original question). See my other comment, what happens if from rust code you dlopen libbar.so ?
that's the same for every language and thus not very relevant. if you have single, static binaries / libraries of course everything is simple, and you'll get linker errors in C++ just like you would in Rust. What is not simple is when you start loading twelve dozen libs at load-time or run-time and it does not seem that Rust defines behaviour any more than C++ in that case.
You seem to be under the impression that dlopen somehow interacts with language undefined behavior; it does not in either Rust or C++.
but it is ! the only reason why ODR is UB in C++ is because the C++ language authors can't force the system linkers (again, whether at link time, load time or runtime, I'm not only referring to dlopen) to perform LTO which trivially makes ODR violations a diagnosticable error.
But as far as I know, neither can the Rust language authors do so - so either the behaviour in Rust is as defined as in C++, or Rust does not support creating standard platform object files that are linked by ld, gold, or whatever (such as D, ADA, Fortran etc all support) which would make a fair amount of use cases impossible - it's pretty common in some HPC circles to link C++ and Fortran directly in the same executable for instance. And then, of course it's easier to define behaviour when you use a reduced set of constraints, but it definitely does not makes something worth bragging about.
Normal, non-FFI-using C++ can hit ODR violations in response to things like typos or subtle mis-uses of `inline` and templates.
Normal, non-FFI-using Rust is designed such that these situations never come up.
My original comment was never talking about FFI in the first place, where yes, both languages are much more at the mercy of what the platform provides. However, in that case the spooky UB ODR violations I was referring to are also not relevant, because you just get normal, fully-defined platform behavior- the expectations of the compiler (and thus the chances for them to be violated, resulting in UB), are different.
$ echo "pub fn my_function() -> i32 { 0 }" > bar.rs ; rustc --crate-type=dylib bar.rs && nm -A libbar.so| grep my_fun
_ZN3bar11my_function17hce21faeb92ac13c6E
$ echo "pub fn my_function() -> i32 { 1 }" > bar.rs ; rustc --crate-type=dylib bar.rs && nm -A libbar.so| grep my_fun
_ZN3bar11my_function17hce21faeb92ac13c6E$ echo "pub fn my_function() -> i32 { 1 }" > bar.rs ; rustc --crate-type=dylib bar.rs && nm -A libbar.so| grep my_fun | c++filt
libbar.so:0000000000047230 T bar::my_function::h4ed6ea856a52cd6b
So adding a single hash to the end of the symbol is a joke?
To be clear, my comment was about the fact that two different functions produce exactly the same symbol name, c++filt or not, which is not what I was told above in " It carefully designs its name mangling such that this doesn't happen in the first place, even in the presence of multiple slightly-different builds of a library."
I have no particular comments on the idea of using hash though I believe that something that changes 95% of chance error in 0.5% of error (I'd assume, as it took me 10 seconds to find a collision) is very bad - you want errors consistently when you fuck up, not once every hash collision as it sounds like a really really big pain to debug when it happens.
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.
> 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.
... 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.
I agree that in practice maintenance has been pretty bad for C++ programs, but I think I understand the kind of reasoning that led so many organizations to pick C++, even if I don't agree with it. It was not about ergonomics or productivity, but about control. The language grants an unprecedented level of power to a small group of elite programmers inside every organization, who author the standard library and headers that all the other programmers use. They're building the smart pointers, the memory allocators, the IO libraries, and the build systems. It's imperative (to them) that their good decisions don't get snowed under the mountain of application code that the more mediocre programmers are going to be producing by the truckload. So it's important to have powerful encapsulation, compile-time checks, and metaprogramming capabilities. Doesn't matter how safe by default or how slow it all is, because the elite add guarantees themselves through the abstractions they write, and don't have to compile a full application very much, if ever.
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.
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?
I don't know why the current Zig approach[1] would be preferable, as you dump some source code that you are now responsible to maintain in your project. Any bigger project project will require writing boilerplate code just to get things built.
It's also a bit of comparing apples to oranges, as Zig's build system doesn't come with a package manager (which I would say makes up a good chunk of Cargo's complexity).
It's also a feature in elixir (Mix). Interestingly elixir also has a comptime concept, so I think having comptime makes having a build library more sensible.
It's the same approach used by nix and guix. Nix is moderately successful. I'm typing this on a laptop running nixos :)
If you're interested in this sort of thing, this is an excellent paper on build system design, although it focuses more on the properties of the build graph than on how the graph is constructed:
https://www.microsoft.com/en-us/research/uploads/prod/2018/0...
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!
Yes, this sort of thing. Basically, you have to have this be deterministic, or you end up with very strange possibilities, possible miscompilations, and in the best case, confusing errors. One option is to simply accept that these things can happen. Another is to restrict what you can do at compile time to ensure that they can't.
An extremely simple example is cross compiling. In Rust, usize is dependent on the architecture you're compiling for. A very simple "just compile and run the program, get the answer, and use it" implementation of compile-time execution will produce a usize of the size of the host, not the target. That's a miscompilation. This example, while being simple, is also simple to fix. But it's an example of how it's not as trivial as "use the compiler to compile the program, then run it." Which maybe isn't how you think of this feature, but how I was, back before I knew anything about this topic :)
> I might be overreacting here though
Nah, I think that you're not wrong. It's just that, when you start applying this super rigorously, you end up in weird places. How can you trust any behavior in a language without a specification? How you can you trust a specification if that specification hasn't been formally proven? How you can trust an implementation of a formally proven specification? How you can you trust that the silicon you're running on does the right thing, even with your bug free, formally proven code?
Everyone chooses somewhere along this axis to be comfortable with. And everyone does something, including "I can ignore these problems because in practice they don't happen to me," to deal with the bits outside of what they consciously choose to focus on.
I get that there are non-obvious problems here, and as you say, this problem specifically has an easy fix, but I'd just like to note how Zig does this:
const std = @import("std");
pub fn main() anyerror!void {
std.debug.warn("{}\n", .{.{
@sizeOf(*usize),
comptime @sizeOf(*usize)
}});
}
Modulo the `.{` weirdness, this probably looks familiar. By default, this prints out 8 and 8 on my system, but if I cross-compile to a 32-bit target, it prints 4 and 4.> It's just that, when you start applying this super rigorously, you end up in weird places. How can you trust any behavior in a language without a specification?
I think this is a social issue for me; if rustc decides one day to change its behavior under my feet it feels like it's my fault for not having written proper Rust in the first place (even if the behavior wasn't properly defined in the first place), but if there's crazy things going on in the compiler due to bugs (that we didn't find, because proofs are hard), then that's not really my fault, in a sense. And of course, if my CPU decides to run my program wrong, that can't really be blamed on me. The end result in these three cases are all the same: the program didn't run as expected, but the blame (I don't want to point fingers, but this is the best word I could come up with) is different, and the probability of this happening is vastly different. I've never hit a CPU bug, but in the little unsafe Rust code I have written, I've had behavior change with a compiler update, which I'm sure is because I hit UB.
And for what it's worth, I would greatly prefer Rust having a proper spec, even if that would increase turnaround time for the language evolution, just to ensure that everyone really is on the same page with respect to what the language really should and shouldn't do. I realize that Rust would rather be careful and make sure that the decisions made are the right ones. I think it's a fair trade-off, but I'm not sure I would have made it, if it were up to me.
Right, what I'm talking about is the implementation of "comptime @sizeOf(*usize)".
That it does the right thing is good! It's easy to accidentally implement it in a way that does not.
> And for what it's worth, I would greatly prefer Rust having a proper spec
We all would :)
I'm sure this happened earlier in the history of English. Of course, humans are much more forgiving than compilers.
> 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?
> 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?
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.
But not much on traits and ADTs(?).
Can you recommend some general learning resources on that?
ADTs are Abstract Data Types[1][2][3]:
> ADT is implementation independent. For example, it only describes what a data type List consists (data) and what are the operations it can perform, but it has no information about how the List is actually implemented.
In the context of Rust it means that the traits, structs and enums are ADTs, while the impls are Data Structures.
Having structs and enums be the way they are in Rust (simplistic and with little extensibility beyond implementing traits) is that pattern matching[4][5] and destructuring is cheap and built in. Pattern matching becomes specially useful when combined with sum types/tagged unions/enums[6][7][8]. On the other corner you have Scala which lets you implement specific interfaces to allow structuring and destructuring for arbitrary types, but that has the same problems as overriding constructors in C++: the performance implications of pattern matching is impl dependent and hidden at the point of calling.
[1]: https://softwareengineering.stackexchange.com/questions/1487...
[2]: https://en.wikipedia.org/wiki/Abstract_data_type
[3]: https://abrickshort.wordpress.com/2005/03/06/abstract-data-t...
[4]: https://en.wikipedia.org/wiki/Pattern_matching
[5]: https://docs.scala-lang.org/tour/pattern-matching.html
[6]: https://www.schoolofhaskell.com/school/to-infinity-and-beyon...
[7]: https://chadaustin.me/2015/07/sum-types/
[8]: https://stackoverflow.com/questions/2502354/what-is-pattern-...
I'm basically looking for a comparison of classes and ADT, I guess.
You said, applying your class-based way of doing things didn't work, so I asked myself how would you do it the ADT way.
The distinction is similar to database design in SQL: the Schema is how the data is laid out and what the relationship between tables is (ADTs) while the queries is the operations performed on them (traits). On the other hand, in OOP there's a higher reliance on encapsulation, making behavior an integral part of what the class is and using inheritance for expansion. When all you have is ADTs, you _can't_ have inheritance, so you end up using composition (which is generally considered better design) and you are more likely to rely on the creation of "new-type" container types for everything. You think of them as a way to describe what the data is, not how you interact with it.
Apologies if this is a bit hand-wavy, I'll try to write a more thoughtful answer at a later time.
[1]: https://medium.com/javascript-scene/the-forgotten-history-of...
Also, ECS design is a thing in game development.
Are ADTs taken these concepts into language design?
Also, I don't mean Rust is harder than C++ or Java. It's just quite different.
If you are used to visualizing software as graphs of functions or classes in your mind for years, switching to the Rust model is quite a change of thinking.
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.
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.
Actually, type safety can be quite useful in such a context: it allows the person who designed the types in use to enforce certain restrictions on the developer who is hurriedly adding code to an unfamiliar codebase, thus reducing the requirement for that developer to understand larger issues in the system.
No doubt type systems can allow you to write safer, more robust code. But they can also allow you to introduce new risks to your codebase, one of which is difficult to read/understand code.
But there are a lot of quite frankly crazy choices, like making (milliseconds) an integer type that is incompatible with numbers. This means to multiply an time with a non-constant integer, you have to cast an integer to millisecond type first, then multiply. Which breaks the brain of a scientist like me and makes me want to throw my computer out the window.
My daily driver is Elixir, and it's not impossible to jump into totally foreign code and debug it. In fact, just the other day I did an interview where the 20 minute technical problem was drilling down from the frontend into the backend of a new-to-me system and finding the logic bug in a database query that was causing the wrong data to be surfaced.
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).
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
The entire macro space in Rust (just like async/await) is in MVP status: they are available and useful already, but their current feature set and approachability isn't the end-state.
I know some will read that and think to themselves "Great! More changes to the language! See, they can't help themselves.", but these changes are about removing restrictions and tapering edges.
Appeal to authority much?
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.
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.
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.
There's a lint you can enable to detct truncation - https://rust-lang.github.io/rust-clippy/master/#cast_possibl...
(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.
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.
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'm not entirely sure I've heard anyone express this before. What do you like about it?
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.
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.
If you really want to avoid it you can install everything as a global dependency, but it's not best practice.
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.
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#.
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...
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.
It is an absolute joy to write code in D. Only downside is people complaining it has garbage collection.
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.
Box, on the other hand, doesn't have any runtime cost. In fact, it's runtime cost is the same as a borrow: both & and Box<T> are just plain pointers, with the extra benefit of having compile time lifetime checking.
[1] https://doc.rust-lang.org/book/ch15-05-interior-mutability.h...
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.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 ?
The only thing that worked was adding a linker argumment: "-C", "link_args=--undefined=<name_of_function>"
That will force the linker to assume it's referenced.
There's also an `#[used]`[1] attribute, but only for static items. If your use case is special, consider open a feature request in Rust repo.
[1]: https://doc.rust-lang.org/nightly/reference/abi.html#the-use...
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.
A response to a security concern should never be "fuck off".
It can be when it is out of context and/or out of proportion.
But it could be more likely that temperamentally you are disposed towards different programing language where security concerns must override any and every issue at hand.
I was always on the IRC though. It could be the discord community is less mature.
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.
Again, the frustration wasn't just from the PR alone - it was also the Discord flame war that ensued prior to the PR.
I usually find community overrated and I've made a conscious effort to separate it from technology for the most part. Zig as a language has certain values that it's built with in mind and those values for the most part are aligned with mine, so this is basically what keeps me interested in it. I would use Rust despite its community if it aligned with my values. I despise the leadership of Elixir and Phoenix but I still use both of those when it makes sense.
With all this said, the IRC channel is a lot less about memery and wild discussions (but also less active) than the Discord server, so if you feel like it you can always just pop into #zig on FreeNode if you have questions and would like to talk about the language.
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.
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.
Please read Andrew's answer and check the linked project management dashboard on GH.
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.
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...
Also: Go (or python) has no UI system or 3D API wrapper in the standard library, but those are (probably) useful for at least as many people as a HTTP server. Does that mean that Go should get UI and 3D-rendering support in the standard library?
Yes, but that "subset" is huge.
>Also: Go (or python) has no UI system or 3D API wrapper in the standard library, but those are (probably) useful for at least as many people as a HTTP server.
Not in the backend/network server world that Go primarily targets...
Maybe I should ask, what is it that makes Python standard lib modules likely to end up like this? The list is pretty long.
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.
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?
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.