Was Rust Worth It?
jsoverson.medium.com
jsoverson.medium.com
I know rust gives memory safety and how important that is, but the ergonomic is really bad. Every time I write some rust I feel limited. I always have to search libraries and how to do things. I cannot just "type the code".
Also the type system can get out of control, it can be very hard to actually know what method you can call on a struct.
I still think rust is a great tool, and that it solves tons of problem. But I do not think it is a good general purpose language.
> But I do not think it is a good general purpose language.
Remember that this is not a sentiment that's shared by everyone. I use Rust for tasks that need anything more complicated than a shell script. Even my window manager is controlled from a Rust program. I say this as someone who has been programming in Python for nearly two decades now. At this point, I'm about as fast in Rust as I am in Python.
While the Rust devs believe such cyclic references are rare - I think this speaks mostly to the problem domain they are focused on. Relational models are everywhere in Apps, they are often common in complex systems software like databases, and they are fairly rare in firmware/drivers/system code code.
There are a few patterns for dealing with cyclic references, but they all end up requiring either unsafe or a main "owner" object which you clean up occassionally (effectively arena allocation). Having now worked in C/CPP - the idea of having unsafe blocks sprinkled around the code doesn't bother me, and many C/CPP components have some form of arena allocation built-in. I just wish Rust learning resources would be more upfront about this.
I don't want to program in anything else anymore. I don't want to deal with obscure C++ error messages, C footguns and lack of ergonomics, I don't want to deal with abstraction hell of Java, or the poor person's typing that python has.
I have been programming in Python for the past 6 years, I know all sorts of obscure details, and with rust, I just don't need to think about all of those issues.
This is factually impossible.
For anything larger than (very) small programs, Rust requires an upfront design stage, due to ownership, that it's not required when developing in GC'ed languages.
This is not even considering more local complexities, like data structures with cyclical references.
Now I'm wondering about demographics. Are people who love Python more likely to love Rust as well?
Rust feels like totally missing the sweet spot for me. It's way too pedantic about low level stuff for writing higher level applications, but way too complicated for embedded or writing an OS. In the former case I would rather take a C++, Java, Haskell, OCaml or even Go, and maybe sprinkle some C, and in the latter case C in macroassembly mode is far more suitable.
I still have a feeling that original vision of Graydon Hoare (i.e. OCaml/SML with linear types, GC, stack allocations, green threads and CPS) would be a much better language.
We as a profession have proven that we can’t write unsafe code at scale and avoid these problems. You might be able to in hand whittled code you write but what happens when other people work on it, it gets refactored, someone pulls in a merge without looking too closely, etc., or even maybe you come back two years later to fix something and have forgotten the details.
Having the compiler detect almost all memory errors is necessary. Either that or the language has to avoid this entirely. Rust is the former class unless you use unsafe, and the fact that it’s called “unsafe” makes it trivial to search for. You can automatically flag commits with unsafe in them for extra review or even prohibit it.
>I know rust gives memory safety and how important that is, but the ergonomic is really bad. Every time I write some rust I feel limited. I always have to search libraries and how to do things. I cannot just "type the code".
You don't have to search libraries and figure out how to do things in Zig?
Rust forces you to figure out ahead of time where each bit or byte is going to go and on which thread and using which mutation scheme. I’m happy to play the game, but it feels tedious for anything short of a parser or a microcontroller.
It messes with my process because I like to get something working before I determine the best API structure for it
I can get 90% of the performance with Swift and it flows much more easily, even though Rust’s type system is more powerful.
You can just clone and be on your merry way; you don’t need to worry about perf-related things if you don’t want to.
Would add Go to the list, but only when I really have to.
Nim and Crystal could be alternatives, but don't seem to have big enough communities, at least for what I do.
However I do agree with the conclusion, Rust is a great language for scenarios where no form of automatic memory management is allowed, kernels, specific kinds of drivers, GPGPU programming, as general purpose, there are more productive alternatives, equally safe.
I've been out of the windows development game for a long time, so I haven't used C# since it strictly required a VM... what's pre-compiled C# development like nowadays? Are there major caveats? If you can emit plain old binaries in C# with no runtime dependencies, that would make it a truly compelling language IMO.
And as another question, what's the cross-platform (mainly Linux) support like in AOT-compiled C#? If it's just as good as in Windows and emits plain executables, I would probably consider it the best place to start for any new project. (Something tells me it's not...)
You can always improve it, but there's no need to. With Rust, I was never happy, even after 5 years, I was still thinking about better abstractions and implementing more traits, making it more generic, etc.
The rust-analyzer language server can autocomplete the available methods for a value.
IMO a good GPR is memory safe (no C, C++, Zig), is easy to use(no Rust), has strong static typing (no Perl, Python, Ruby) and is "stable" (no Scala). Lots of choices remain: Java, Kotlin, Ada, D, OCaml..
For me, it shares the most important benefits of Rust but with quite a lot more ergonomic coding model.
But me, I prefer to manipulate registers directly. Especially with "exotic" MCU where you have to execute write/read in specific number of CPU cycles. Rust makes that very hard.
Not sure if much has changed (it was a while back), but my biggest problem was with finding and using 3rd party libraries (mostly for the boring stuff like DB connectivity, JSON/YAML parsing, logging, etc.).
E.g. even now if I search for "zig mysql library", the tops hits are all about people discussing it on reddit, than any actual library.
> I always have to search libraries and how to do things.
Once you pass the initial curve with crutch like Copilot, then you can be almost as productive (if not more, considering refactoring and testing) with your native first coding language
Having used Rust for a long time it’s definitely the biggest source of confusion and headaches.
Maybe it was a reaction against the Java-style reverse DNS notation, which is verbose and annoying, but a more GitHub-style user/group namespace prefixing package names would have been the nice middle ground.
I send the analysis to the crates.io team and pointed that they have a no-automation policy.
They told me that it was not sufficient proof that someone was squatting those names. That's my problem with crates.io is that they have a clear policy and they don't enforce it so all the short/easy to remember names for crates are already taken and there is nothing you can do to get it.
> Using an automated tool to claim ownership of a large number of package names is not permitted.
And
- Hey, I found that someone created crates at a rate of about one every 30 seconds for a period of a week straight.
- That's not sufficient proof of squatting.
Whoever answered that, was either supporting the squatter or explicitly in favor of the practice. I cannot conceive that someone would get that evidence in their hands, and in their right mind think that the claim is bogus. Hell, I'd even be willing to suppress the squatter with evidence of one new crate created every 30 seconds for one hour!
The only reasonable conclusion to make is that they didn't really care. But then don't save face and claim that you do. That's hypocrisy.
Not naming names, but I know several people working to put Crates.io out to pasture.
There's a level of playing nice with them for the time being (eg. build reproducibility), but it's only KTLO.
Crates.io needs to die for Rust to thrive. They're a bungled, mismanaged liability. New code, new leadership.
What's that? I have scripts that automate publishing of new release of my crates. And I think many projects have.
I suspect it was less a reaction against anything and more just following the norms established by most other package managers. NPM, PyPI, RubyGems, Elixir's Hex, Haskell's Cabal... I'm having a hard time thinking of a non-Java package manager that was around at the time Rust came out that didn't have a single, global namespace. Some have tried to fix that since then, but it was just the way package managers worked in 2014/2015.
The implication here is that namespaces in package managers weren't a known concept. Outside Java, NPM - probably the biggest at the time - not only supported them but was actively encouraging them due to collective regret around going single-global in the beginning. Composer is another popular example that actually enforced them.
Not only was namespacing a known widespread option, with well documented benefits, it was one that was enthusiastically argued for within the Rust community, and rejected.
"We're pretending security is not an issue." has been the feedback every time this is raised with the Cargo team.
To be honest, it's turned me off Rust a little bit.
The attitude of "Rust is memory-safe, so we don't need any other form of security." is not a good one.
Technically not in the same category, but Docker Hub (2014) had namespaces.
1. Flexible version (of requirements) specification;
2. Yes, source code had domain names in packages but that came from Java and you can technically separate that in the dependency declaration;
3. You can run local repos, which is useful for corporate environments so you can deploy your own internal packages; and
4. Source could be included or not, as desired.
Yes, it was XML and verbose. Gradle basically fixed that if you really cared (personally, I didn't).
Later comes along Go. No dependency management at the start. There ended up being two ways of specifying dependencies. At least one included putting github.io/username/package into your code. That username changes and all your code has to change. Awful design.
At least domains forced basically agreed upon namespacing.
So many inferior dependency management systems for other languages have come along later, and learned nothing from those that came before it.
By the time you're going to production, your vetted and locked dependency should be living in your own cache/mirror/vendored-repo/whatever so that you know exactly what code you built your project around and know exactly what the availability will be when you build/instantiate your project.
Your project shouldn't need to care whether GitHub fell out of fashion and the project moved to GitLab, and definitely shouldn't be relying on GitHub being available when you need to build, test, deploy, or scale. That's a completely unnecessary failure point for you to introduce.
Systems that use URL-identified packages can work around some of this, but just reinforce terrible habits.
[dependencies]
uniffi = { git = "https://github.com/mozilla/uniffi-rs" }
You can also specify revision/branch/etc.Alternatively, you can do:
[registries]
maven = { index = "https://rust.maven.org/git/index" }
[dependencies]
some-package = { index = "maven", version = "1.1" }
Obviously Maven doesn't host any Rust crates (yet?), this is just a theoretical example. Very few projects bother to host their own registry, partially because crates.io doesn't allow packages that load dependencies from other indices (for obvious security reasons). The registry definition can also be done globally through environment variables: CARGO_REGISTRIES_MAVEN="https://rust.maven.org/git/index". Furthermore, the default registry can be set in a global config file.In theory, all you need to do is publish a crate is to `git push upstream master`, and your package will become available on https://github.com/username/crate-name (or example.com/your-package if you choose to host your git repo on there).
Personally, I don't like using other people's URL packages, because your website can disappear any moment for any reason. Maybe you decide to call it quits, maybe you get hit by a car, whatever the reason, my build is broken all of the sudden. The probability of crates.io going down is a lot lower than the probability of packages-of-some-random-guy-in-nebraska.ddns.net disappearing
Naming things really is one of the hardest problems. This crates thing is a special case of Zooko's Triangle: https://en.wikipedia.org/wiki/Zooko%27s_triangle
Crates.io names are human-meaningful and everyone sees the same names, but it's vulnerable to squatting, spamming, and Sybil attacks.
You could tie a name to a public key, like onion addresses do, but it's unwieldy for humans. (NB, nothing stops you from doing this inside of crates.io if you really wanted)
You could use pet names where "http-server" and "http-client" locally map to "hyper" and "reqwest", but nobody likes those, because they don't solve the bootstrap problem.
It's a problem with all repos because when you say "http-server should simply be the best server that everyone likes right now", you have to decide who is the authority of "best", and "everyone", and "now". Don't forget how much useless crap is left in the Python stdlib marked as "don't use this as of 2018, use the 3rd-party lib that does it way better."
So yeah... probably will be a problem forever. As a bit of fun here are some un-intuitive names, and my proposed improvements:
- Rename Apache to "http-server"
- Rename Nginx to "http-server-2"
- Rename Caddy to "http-server-2-golang"
- Rename libcurl to "http-client"
- Rename GTK+ to "linux-gui"
- Rename Qt to "linux-gui-2"
- Rename xfce4 to "linux-desktop-3"
Then you only need to remember which numbers are good and which numbers are bad! Like how IPv4 is the best, IPv6 is okay, but IPV5 isn't real, and HTTP 1.1 and 3 are great but 2 kinda sucked.
Very simple. If a company as big as Apple can have simple names like "WebKit", "CoreGraphics", and "CoreAudio" then surely a million hackers competing in a free marketplace can do the same thing.
This is true that with no namespace anyone can end up squatting a cool name, but with namespace you end up in an even worse place: no-one end up with the cool names, and it makes discoverability miserable, because instead of having people come up with unique names like serde, everybody just name their json serialization/parsing library json and user now needs to remember if they should use "dtolnay/json" or "google/json" (and remember not to use "json/json" because indeed namespace squatting is now a thing), and of course this makes it completely ungoogleable.
We've had the namespace discussion for hundreds of time in the various Rust town squares, and the main reason why we still don't have namespace is because it doesn't actually answer the problem it's supposed to address and if you dig a little bit you realize that it even makes them worse.
Having a centralized public and permissionless repository opens tons of tricky questions, but namespaces are a solution to none of them.
What's the problem with that? You will have to explicitly state intention which is always a good thing.
> and of course this makes it completely ungoogleable
You are aware that just pasting username/projectname gives you exactly what you are looking for in the several top search engines, correct?
> We've had the namespace discussion for hundreds of time in the various Rust town squares, and the main reason why we still don't have namespace is because it doesn't actually answer the problem it's supposed to address and if you dig a little bit you realize that it even makes them worse.
OK, if you say so. Is this worse state of things documented somewhere?
Do you really think nobody has ever been able to google a Go import path? Some of these arguments are ridiculous.
Perl's whole ecosystem is amazing compared to other languages. It's a shame nobody knows it.
Neither node nor maven ever bothered to solve this, so we end up wandering the Wild West wondering when it will be that HR, or legal, or architecture comes knocking on the door to ask what we were thinking having a dependency on a dynamic version of Left Pad.
I'd kinda like to see what Cloudflare and Let's Encrypt could come up with if they worked together on at very least a white paper and an MVP POC.
Pay somebody either internally or externally to maintain a repo of all your dependencies and point your code at that. You won't get a left-pad incident. You won't get a malicious .so incident (unless you mirror binaries instead of source code).
Like if you ran out of screws to make your product with do you walk around the street and scrounge up some? No, you go to a trusted vendor and buy the screws.
As other mentioned though, a typosquatting is a much bigger problem and namespacing offers only a partial solution. (You can still create a lookalike organization name, both in npm and in Github.)
Or it is just a placeholder from a squatter.
Go has namespacing mostly because for a long time it didn't have a package manager at all, so people just used a bunch of ad hoc URL-based solutions mostly revolving around GitHub, which happens to have namespaces and also happened to lend itself to aliasing (because a whole GitHub URL is too long).
If you want to look at an actual example of namespacing done well, Maven/Java is the place to look. There is no aliasing—the same imports always work across projects.
brandonq/xml vs parsers/xml with no clue if one is better then another.
Also possibly with some confusion over whether things with the same name are forks or not. May make it a little more difficult to Google. Why not just have brandon_xml vs xml-parser and have a community list of best and most popular libraries?
I guess the only issue is that some generic/obvious package names are bad packages. That can have been avoided if they banned/self-squated most of the names. I suppose if you use dns namespaces and actually tie it to ownership of the domain name it might make sense but that would also cause issues(what if you forgot to renew the domain?).
We could even have `rust.xyz` for crates that are decoupled from `std` but still maintained by rust core devs such as `regex`.
You will have this problem only once. After an initial research you settle on one and move on with life.
I don't get how having to vet a dependency is going to be more difficult than before. The process is 99% the same, you still have to do the research work initially in both cases.
It's definitely a good thing that people choose new unique names for crates rather than dijan/base64 vs dljan/base64
Do understand the desire of having a crate for audio manipulation called "audio" but at the same time how often do we end up with "audio2" anyway? It's an imperfect solution for an imperfect world and I personally think the crates team got it right on this one.
It's really as simple as this: many libraries are generic enough implementing something that already exists. Let's say you want a library to manage the SMTP protocol. On crates.io, of course someone has already taken the "smtp" crate (ironically, this one is abandoned, but has the highest download counts, because it's the most obvious name). Let's say you disagree with the direction this smtp crate has gone, and you make your own. What do you call it?
Namespaces solve this problem. You'd instead of have user1/smtp and user2/smtp competing in feature sets. You can even be user3/smtp if you don't like the first two.
This is precisely what Java enables too. The standard library is in com.java.*; if you don't like how the standard library does something, you can make com.grimburger.smtp and do it yourself. If you choose to publish to the world, all the more power to you. It doesn't conflict with the standard library's smtp implementation.
https://samsieber.tech/posts/2020/09/registry-structure-infl...
The first section says it discourages forking. I consider this to be bad. Nobody’s code should be more important purely because it squatted a better name.
The Identity section actually makes the case that flat registries make naming harder.
The section on Continuity is “we’ve tried nothing and we’re all out of ideas”. Make up an org name and grandfather all packages in the flat namespace into that special org. Also this is already a problem because packages in the flat namespace do get abandoned, then forked, and then we have the associated issues.
The section on Stability seems to take it as a given that crates.io should be the only registry. I don’t. It also seems to conflate cargo with rustc for the benefit of the argument.
The squatting section describes only anti-features and I don’t consider the author’s legitimate use cases to be legitimate reasons to squat.
I think the only legitimate problems that need addressing are the ergonomics of accessing namespaced packages throughout transient dependencies and backwards compatibility with non-namespaced code. But the fact that these are real problems does not, to me, make a flat namespace a “feature”. It’s just easier to implement.
It’s okay for it to be a mistake that takes effort and time to fix.
> I questioned my sanity every time I circled back around to the Clippy issue above. Surely, I was wrong. There must be a configuration I missed. I couldn’t believe it. I still can’t. Surely there must be a way to configure lints globally. I quadruple-checked when I wrote this to make sure I wasn’t delusional.
You create a .cargo/config.toml in the workspace root so it covers all your crates.
inside the file:
[build]
rustflags = ["-Wclippy::lint_name_to_warn", "-Dclippy::lint_name_to_deny"]
The only limitation is that rustflags are not additive, so if you have other sources of rustflags like the RUSTFLAGS environment variable it will overwrite this setting.It's fine. It won't be the only language I'll be proficient in while being averse to it at the same time. But I heard so many people proclaiming their love for it that I expected to enjoy it, too.
For me, the key to understanding the borrow checker was understanding the underlying memory model. Rust memory model is the same as that of C, with some extensions for abstractions like generics. The borrow checker rules seem arbitrary at first. But it's deeply correlated to this memory model. The real value of the borrow checker is when I trigger it unintentionally. Those are bugs that I made due to lapse in attention. What scares me is that another language like C or C++ might simply accept it and proceed.
Yet another pleasant side effect of Rust's strict type system and borrow checker is that they gently nudge you to properly structure your code. I can say for certain that Rust has improved my code design in all the languages that I use.
Also, just generally, it tends to make even simple things pretty complex. I understand why and am not really objecting to that, but it does make using it a bit like running with lead shoes.
I expect that the latter part may get better as I use it more (but perhaps not -- there are other languages I'm fully competent in but dislike for similar reasons).
I am a former C and C++ programmer who lived calling into pthread almost every week for a decade. I use async rust everywhere now.
I don’t get the hate that async gets. In my opinion, everyone should be using async for everything. Including stuff that’s seemingly single threaded “simple” stuff.
Similarly, I find tasks that use or reuse large buffers to avoid the performance hits from allocation, often benefit from old fashioned thread pools. Bump or shard allocators can make this work ok, but in the cases where you are cpu bound on tight loops of vectorizable operations, thread pools perform better. Async is a good tool, but there are contexts it isn't optimal for.
For instance, you apparently cannot `await` in the main function without a decorator you import from, you guessed it: Tokio.
Once you stop fighting the fact that tokio = async rust for 99% of cases, things are quite smooth.
1. Fix rhai::Engine so it is Send (if !Send is unintentional)
2. Use tokio::spawn_blocking or normal threads to run the rhai::Engine bits
3. Don't hold the rhai::Engine across an await point.
Which one depends on rhai Engine details and what you want to accomplish.
Doing a block on inside a new thread seems unlikely to do anything useful (unless there's some undisclosed detail that makes it reasonable).
I encourage you to ask about this in the rhai repo in a discussion or issue.
From a quick look at the source, it looks like it switches between using Rc/RefCells and Arc/RwLock.
[0] https://docs.rs/rhai/1.16.2/rhai/#enable-special-functionali...
I learned the opposite, that programmers tend to make everything unnecessarly async when it's not needed causing more complexity and mental load.
Does "use async rust everywhere now" imply "use tokio everywhere now?" Honest question.
This is handy for I/O that can be interleaved and cancelled. You can (ab)use it for other things like generators or various DIY multitasking operations. It can also be a state machine generator (e.g. AI of actors in a game).
But I think OP just meant async for typical networking and DB interfaces. And yes, this usually implies the Tokio dependency.
Rust's error messages are one of a kind. There no other compiler which comes even close.
As a side note, i used latex lately, it's error messages are horrendous. What a nightmare to figure out what's wrong by inspecting the error.
Generally good, but man do I hate how any error in my async function causes every recursive call site to generate an error about how the Future is no longer Send and Sync. Literally an entire console scrollback of errors with the actual syntax error buried somewhere in the middle.
My main gripe is that I still don't fully comprehend lifetimes and the compiler can't really help me every time, because it (understandably) errs on the side of caution.
This is a common refrain in C++ testing: if it compiles then it's probably correct.
> Rust has accounted for so many errors that many common test cases become irrelevant
In practice, if you think this way I think it's a sign that you aren't testing the right things in those other languages. You should be testing business logic, not language stuff.
If you look at your test code and think "I would test this in JavaScript, but I don't need to do that in Rust" then just delete the test.
I’ve heard this in Haskell and in Rust. I’ve never heard it applied to C++…
How do null pointer exceptions arise?
Inconsistent data: you have code paths that implicitly assume invariants. Trivial cases like “this field is not provided” and more complex ones like “these fields have a specific relationship”.
You can often move those assumptions into the data structure in any language.
Then your tests become a matter of generating and transforming data from a holistic perspective instead of micromanaging individual code paths.
Remember unlike most languages you'd be familiar with C++ has IFNDR, which has been jokingly referred to as "False positives for the question: Is this a C++ program?". A conforming C++ compiler is forbidden from telling you in some† unknown number of cases that it suspects what you've written is nonsense, it just has to press on and output... something. Is it a working executable? Could be. Or maybe it's exactly like a working executable except it explodes catastrophically on Fridays. No way to know.
† The ISO standard does identify these cases, but they're so vague that it's hard to pin down everything which is covered. My guess is that all or most non-trivial C++ software is actually IFNDR these days. Just say No to the entire language.
It's not quite that bad - a conforming C++ compiler is permitted to error out and not compile the program. It just doesn't have to.
Rust is quite transparent in what it does, and is very conservative with compiler magic. The language doesn’t do heap allocations, doesn’t do reference counting, it doesn’t even have implicit numeric type conversion. It won’t implicitly copy types that did not ask to be implicitly copyable, and even that is only legal for types that can be copied with a simple shallow memcpy.
Rust uses zero-cost abstractions all over the place, which means it’s predictable what code they will compile to, and that will be typically something simple. Std types have well-known basic layout, so you know that e.g. iteration over a Vec is going to be a loop incrementing a pointer, and there’s no implicit parallelism.
Describing borrowing as “compiler knowing better than the programmer” is a weird way of looking at it. Borrowing is like type checking. You declare a type to be temporary, and try to use it as long-lived, you get an error. It’s the same as if you declare function to return struct Foo, but returned struct Bar instead. Yes, compiler “knows better than you”, because you just wrote a bug.
Borrowing still compiles to direct pointer usage without a GC (it’s literally guaranteed to be identical to a C pointer in C ABI structs and functions), and you can override lifetimes with unsafe if you think know better than the compiler.
There is already a crate providing parallel iterators. You just rename the iter() call and that's it. I don't agree it should be implicit though.
But to your point, the convenient defaults are very different. Unsafe typecasts require a lot of ceremony and careful thought in Rust, and they have to follow more rules than in C. In particular, references are all effectively "restrict" in Rust, and it's really easy to screw that up when you do unsafe cast from raw pointers to safe references, which is a big incentive not to write code like that if you have a choice.
Whereas Rust is a standard with multiple implementations?
If anything I want more explicit control, alleviated with an even more expressive type system. Ideally rusts type system would just be a prolog variant imho.
https://en.wikipedia.org/wiki/Category:Compiler_optimization...
Otherwise we should trust compilers to know better.
Bash with types (especially floats), fewer edge cases, functions with explicit parameters, simple command line flags...
The Kotlin type system is comparable to Rust without the borrow checker. It has non-null types, generics, etc.
It's not really a "product" per se but there's an old version for download and some lightweight docs here:
We've never worked out what to do with it. Ideas and feedback welcome. It's pretty nice to use, albeit you need to use IntelliJ to edit scripts if you want IDE features.
I know it’s still not 1.0. Then just get fish. At least would save some keystrokes.
A good C compiler does this when you turn on all the flags. I like languages/compilers that let you selectively disable the screaming and let you write bad code on purpose. Bad code that works but can be written fast is often better than perfect code that takes forever to write. Once you have a bad but working POC, you can make it less bad.
"It’s got no problem attracting new users, but it’s not resulting in dramatically improved libraries or tools. It’s resulting in one-off forks that handle specific use cases."
Age has nothing to do with that. Attracting core devs is hard, and putting lots of effort into making it attractive is necessary. On top of that, cultural conventions are set by the early adopters, and a lack of convention is often just as sinful as a bad existing convention.
Take Python for example. They took a lackadaisical approach to development and runtime environments, and as a result there's 50 competing ways to develop or run a Python program. Their most-used package repository, PyPI, has been a mess for years. Nobody builds on top of existing packages, names make as much sense as a random word generator, the ecosystem is rife with malware, you can't even search for a package on the command line, etc etc. None of that is the language's fault, it's the community and core team's fault for sitting on the sidelines rather than leading. Culture matters more than the tech it's centered around.
(I'm not trying to pick on them, I just know their problems better. C has been around for a half century and its community never really put together half the solutions more modern languages did)
Rust supports that. Just mark everything unsafe.
You can instead wrap everything in Arc<Mutex<T>> and .clone() liberally, though.
I'm not too concerned with performance or safety (since my code hasn't been seen very often).
I use rust just because it has a lot of "libraries that help me develop".
While other language libraries have a poor search experience and ranking system, Rust has a great library search system.
I use it because I don't have to read blog posts like C/C++ or java to find libraries that help me develop, or wade through unnecessary libraries like C# or go, I can use the rankings, and it has a good documentation system.
As you've said, finding great libraries is easy, and so is adding/building them. I find this a nice change from e.g. Python (where there are so many way of different competing ways dealing with packaging/dependencies).
Also thanks to the error handling, sum types and the like, I don't have to worry so much that I've deal with all errors, handled all enum variants, etc., the compiler takes care of that.
At the language level, the big problem is back references. Sometimes you do need them, and the only safe way to do them at present involves reference counts in the forward direction and weak references in the back direction. Then you have to call .borrow() and .upgrade() too much. I'd love to see a static analysis solution to that.
(Rough outline of such a solution: Owning object belongs to Owner trait. Owned object belongs to Owned trait. Owned object has .owner() and .owner_mut() functions which retrieve references to the owner. Owners probably have to be pinned, so they can't move while a back-reference exists. If an Owner changes a reference to an Owned object, the back reference is automatically updated.
That's the easy part. Now figure out how to prove by static analysis that a specific use of this does not violate Rust's no-aliasing rules (N read-only, or 1 mutable). This looks do-able for the non-mutable case, because having a non-mutable reference to both owned and owner is OK. Mutability, though, is tough. Anybody thinking about this?)
Is it fair to say that Rust failed to supply a robust set of standard libraries comparable to other modern languages? Or was the language aimed at level geared towards implementing rather than providing libraries? If it's truly a systems language, then what library features are essential, and what are 'nice to have'?
They often have better quality than what I find in "mature" packages in npm or PyPi.
Rust kind of has a perfectionist touch to it which makes it really hard to say "this public API is stable". So people stay at 0.. way longer than normal.
I'm fine with that.
The lack of namespacing and heavy namesquatting is rather what's annoying.
But it’s a trade off because there are concrete upsides to a large standard library. I wrote about this more - Rust has a small standard library (and that’s ok) - https://blog.nindalf.com/posts/rust-stdlib/
Out of curiosity, is there any existing language (including research languages) that can do this already?
You can do this in Rust at run time, with Rc, Weak, and .upgrade().unwrap(). If none of the .unwrap() calls fails, you did it right. If you can prove the unwrap calls can't fail, you don't need the run time checking. So the goal is to check at compile time something you can now check at run time at higher cost. That's what Rust is all about.
I complained about this here[1]. I'm told "Tokio won, it's not a problem anymore." Wrong. It's still a problem and it's a killer.
And so, for some or all of these reasons, right or wrong, various Rust libraries wed themselves to runtimes other than Tokio.
You can thunk around these things, but it's miserable, yielding subtle, inscrutable code that you will not understand in six months when you have to maintain it. And the real fun begins when you need to handle Err, and all the abstraction leaks around Send and threads ruin your day; more thunking, Boxing errors (omfg) and even subtler and more inscrutable code.
Tokio monoculture is a serious problem.
First, I have to wonder whether any of this can be legitimately elevated to the level of a "culture." Right now, according to GitHub "insights," tokio-rs has approximately 2.0 regular, every day contributors. And that's the most widely used Rust async runtime. Everything else is likely even more thin.
Second, Tokio has a lot of share because it was early and aggressive in actually delivering usable async documentation and code. I recall, years ago, reading Tokio documentation on how futures worked and grasping these concepts all the way from file descriptors one might epoll() in C, up to the Rust abstractions, and thinking "hell, I could write an executor from first principles based on this." Tokio earned the advocates it has, as oblivious to the real state of things as some of them might be.
I think the real problem is that async Rust is incomplete. As I said elsewhere, async Rust syntax is fine. The implications of async Rust exposed some papercuts in the language that have had to be dealt with since, but the core syntax is fine. I believe that can be attributed to the serious minded attention that the syntax received from people way, waaay up the cognition curve. The part that didn't get enough thought was async runtimes. In an ideal world, one would develop a Rust library that utilizes and/or implements asynchronous calls and transparently, flawlessly run on any correctly implemented runtime alongside any other number of libraries also utilizing and/or implementing async calls.
That is not the case, and the damage that's doing is severe. For every one person, such as myself, that will dare attempt to articulate this pain and, in the process, certainly revealing clear evidence of blatant ignorance, a thousand others just silently gave up.
I wonder what kind of tests the author was writing? The test cases I write for my code is for weird edge cases I detect during testing, like parameter over or underflows, correctness of parsing functions, etc. Things that do fail. I don't get why you'd write tests for trivial things that can't fail.
Even when you don't care about performance, another issue that comes up sometimes is keeping track of mutable state. If you've ever relied on bytes instead of bytearray or tuple instead of list* to guarantee that no mutation is happening in some Python code, you know what I'm talking about. Rust can give you a similar level of control over who gets to mutate what, without making you change the type of your data or pay the cost of copies. It's basically the const/non-const distinction from C, but much stricter. Another way of saying the same thing is that Rust gives you a lot of the legibility/correctness benefits that you'd expect from a functional programming language, but you get to code in the usual imperative style.
* And even then, that only prevents assignment to the elements of the tuple. You can still mutate an element internally if it's not also an immutable type.
The experience has been far more than just borrow checking, it’s opened up my mind to a lot of concepts that my previous trials with Java, JavaScript, and Python fully obscured in arcane ways. Rust’s compiler is wonderful, and once you get past the ergonomic struggles (which I did by just going through Advent of Code), you’re set.
* Everything is a struct with concrete types. This is closest to C. It benefits from the improved memory safety, without sacrificing speed.
* Everything is a Box<Rc<T>>. This is closest to dynamic garbage-collected languages, but with vastly improved performance.
* Everything is an “impl Trait”. This is closest to templated C++, but with much better ergonomics.
So, while I think I agree with your specific statement that the improved memory management may only be fully appreciated by those that cut their teeth on C’s segfaults and dangling pointers, I don’t think that’s the only benefit of Rust as a language.
If you've never used an ML family language then Rust might still be a breath of fresh air even if you don't care about memory management.
Rust is great for services such as API endpoints, where low latency and high throughput at a low cost are more important that hot reload during development.
I think it's a matter of perspective/background. Depending what language you're coming from you might appreciate a lot of the more modern language features, or binary size, or performance. On the other hand, you might already be working with a pretty cutting edge language with a garbage collector or other and decide that Rust has some cool benefits but not worth the switch.
> It also looks like (soon) you’ll finally be able to configure global lints for a project. Until now, you had to hack your solution to keep lints consistent for projects. In Wick, we use a script to automatically update inline lint configurations for a few dozen crates.
It's trivial to configure global lints per crate. The author's problem comes down to having multiple crates per project. Which is something people do sometimes, and should be supported, but there's a big difference between the two interpretations. I would go so far as to say most Rust projects are only one crate (and so support "global" linter configuration)
Do you need bare metal performance? Then why use Rust when you could use C or C++, which have much larger ecosystems, platform support, more mature tooling, etc.
Do you need BOTH memory safety and baremetal performance at the same time? Then there really aren't many other options besides Rust.
But what I've started wondering lately is: are there really that many situations where you actually need both of those things at the same time? C++ tends to get misused a lot, but I feel like the same thing is happening with Rust.
Personally, I've never met anyone who can ensure memory safety in their C/C++ without onerous restrictions even more severe than those rust imposes, but maybe you're superhuman.
That, and the borrowing/safety aspects of Rust also shake out into threading/concurrency, where the compiler does a pretty good job of forcing you to adopt better practices in regards to state sharing, locking, concurrency.
Cargo, though... I am coming around to thinking isn't great -- workspace support remains half-assed, especially... and crates.io is amateur-hour for reasons many people have pointed out elsewhere on this post. I have been tempted to switch my personal projects to bazel, with 3rd-party deps checked in/submoduled.
I think that’s an unusual way frame that requirement.
All programs need to handle memory _correctly_. Very few seg fault as part of expected operation.
And it offers those things along with a fantastic type system, and great tooling.
This isn't necessarily a problem. C++ and Rust can coexist pretty well in practice, and Rust is a good entry point for learning how to write systems software.
So I guess I wouldn't be surprised that the applications are trending that way, too, as that is where growth is happening right now it seems.
Are there any efforts to overhaul or completely rethink this?
Algebraic effects would solve this, but I don’t know any language other than OCaml that is working on that approach.
In general I’ve noticed that there are a lot of code generation modules that can generate very cryptic error messages if you stay clear of their happy paths.
tl;dr: You can have different (nicer to program) abstractions, but you can't have them in the same language you use to write firmware for a $10 electronic device, or Linux drivers, and we already have languages like Go and Javascript whereas we did not have a safer alternative to C++.
For typical applications I am learning nim, which has python syntax, c speed and size, and could be memory safe too.
I really feel nim deserves more love, for that you can balance coding-speed(write like python script), size , performance and security, and cal leverage c and c++ libraries without FFI, no other languages can have those at the same time.
Writing performance-sensitive code in C/C++ and calling it via interop used to be the way to go during .NET Framework days but since then has become a performance trap.
Especially for small methods, calling them through interop is a deoptimization because they cannot be inlined, and involve GC frame transition (which you can suppress) as well as an indirect jump and maybe interop stub unless you are statically linking the dependency into your AOT deployment. In case the arguments are not blittable to C - marshalling too. Of course, this is much, much faster than anything Java or Go can offer, but still a cost nonetheless.
It is also complicates the publishing process because you have to build both .NET and C parts and then package them together, considering the matrix of [win, linux, macos] x [x64, arm64], it turns into quite an unpleasant experience.
Instead, the recommended approach is just continuing to write C# code, except with pointer and/or ref based code. This is what CoreLib itself does for the most performance-sensitive bits[0]. Naturally, it intentionally looks ugly like in Rust, but you can easily fix it with a few extension methods[1].
[0]: https://github.com/dotnet/runtime/blob/main/src/libraries/Sy...
[1]: https://github.com/U8String/U8String/blob/main/Sources/Compa...
Good to know you can remain within the framework and get decent performance though with the unsafe pointers/refs. Would be interesting to see a good benchmark using only C# with latest features and Rust, although I cognisant of the fact there is more to it than pure performance (binary size, dependencies, GC etc).
I feel this one, something about spending too much time in JS makes me not want to write clean software..
Lifetimes need something to stand out as an identifier. 'a is weird, but works. Whether it could use a different ASCII sigil is a bikeshed problem.
Types borrowed <> from C++ to look less weird to C++ programmers. But this again is just a surface level issue. Semantically, the wrapper types are incredibly useful. Having all nested types spelled out is convenient when reading code – you know what you're getting and what are the standard properties of it.
I programmed in C and C++ for years, to compiler messages don't make me feel like I should be taking them personally. Sure beats having a runtime error any day.
I wonder if those with that attitude have come from a background where they used dynamically typed languages or non-compiled languages more, or it is something even people with a wider experience find particularly onerous with Rust?
- I loved here. I am still is. But we needed a divorce.
I still like threads, but the I'm old and uncool.
Hey, I resemble that remark.
But I disagree with it. Green threads give you all the advantages of async, but with less of the hairs. In particular no special syntax or change of programming style is required. Yet underneath green threads and async just different styles of event driven I/O, so both run at similar speeds and excel at the same tasks. (Actually green threads should run faster, as storing state on a stack is generally faster than malloc.)
I have no idea why Rust abandoned green threads in favour of async. Actually, that's a partial lie - there have been far too many words wasted on explaining why. The problem is the reasons they give look to be an caused by design decisions they made in their implementation. The primary objection seems to be speed. The current async is indeed faster than their old green thread implementation. But that was caused by their choosing to avoid coloured code in their green threads (maybe they were copying Go?). Other objections were similarly to do with the implementation they threw away, not green threads themselves.
They require more memory over stackless coroutines as it stores the callstack instead of changing a single state. They also allow for recursion, but its undelimited meaning you either 1) overrun the guard page and potentially write to another Green thread's stack by just declaring a large local variable 2) enable some form of stack-probing to address that (?) or 3) Support growable stacks which requires a GC to fixup pointes (isn't available in a systems lang).
> green threads should run faster, as storing state on a stack is generally faster than malloc.
Stackless coroutines explicit don't malloc on each call. You only allocate the intial state machine (stack in GreenThread terms).
> The primary objection seems to be speed
It's compatibility. No way to properly set the stack-size at compile time for various platforms. No way to setup guard pages in a construct that's language-level so should support being used without an OS (i.e. embedded, wasm, kernel). The current async using stackless coroutines 1) knows the size upfront due to being a compiler-generated StateMachine 2) disallows recursion (as that's a recursive StateMachine type, so users must dynamically allocate those however appropriate) which works for all targets.
Wow, as the author of that feature, I'm surprised to see someone was so passionate about it. I've found that many times I've been having to tell people why they should care about it.
> I don’t know why. Maybe the pressure to maintain stable APIs, along with Rust’s granular type system, makes it difficult for library owners to iterate. It’s hard to accept a minor change if it would result in a major version bump.
There is a tension between people wanting features and people not wanting to deal with version bumps. I've seen this a lot in maintaining clap, especially when it went from unmaintained for years to having active releases.
As for cargo, the compatibility guarantees are tough. Take the lints table. We can't throw in a first try, knowing we can fix in in a cargo 2.0. We are tied into the rust project itself which means we have the same compatibility guarantees. This is one reason we generally encourage trying ideas out in third-party plugins before we integrate them in directly since they can break compatibility.
> You can’t even publish a crate that has local dev dependencies
You can; cargo automatically strips them. However, if you tell cargo that there is a version of it in the registry (by setting the version), then it must be published. This is why when I redesigned `cargo add` for being merged into cargo, I made it so `cargo add --path ../foo --dev` will not add the `version` field. We do need to find ways to clarify that the purpose of the version field is for looking it up in the registry.
Allowing the dev dependencies to be stripped also helps with issues of circular dev-dependencies.
> However, many developers break large projects down into smaller modules naturally, and you can’t publish a parent crate that has sub-crates that only exist within itself.
We do have an RFC for this: https://github.com/rust-lang/rfcs/pull/3452
The most complex part is the Index, figuring out how to represent it in the metadata tables we maintain so we avoid having to download every `.crate` file.
I also worry there might be tech debt around assumptions of there being a single version of a package when nested packages will easily break that.
> You can see the problem manifest in the sheer number of utility crates designed to simplify publishing workspaces. Each works with a subset of configurations, and the “one true way” of setting workspaces up still eludes me. When I publish Wick, it’s frequently an hour+ of effort combining manual, repetitive tasks with tools that only partially work.
I'm a bit confused on this point. While there are things to improve around publishing workspaces, I'm not sure how this relates to setting workspaces up or what problems they've had with that. I'd also be curious what problems they had with releasing packages. I don't think I've seen issues from them in cargo-release's Issues.
One thing I don't appreciate is nonorthogonality of certain uses and control structures that cannot be refactored into a function despite structural equivalence without introducing a multiple borrow conflict.
Need performance, security, and reliability? Build that part in Rust, and have the rest be executed and orchestrated in python. It also forces great design patterns in the form of encapsulation and a strong API.
I think the idea of being monolingual in codebases is a silly limitation and lots of development teams would be a lot more productive if they embraced the idea of polylingual codebases.
Years ago I tried compiling a rust sample program. The binary size was one thing that put me off.
Today, I see the issue of large binary size has been addressed. The binary I get is sufficiently small.
If this issue of ~500M standard library is fixed, e.g., if some of it is made optional not mandatory, I will give rust another try.
3 packages will be downloaded:
3 packages will be installed:
libexecinfo-1.1_3
libexecinfo-devel-1.1_3
rust-std-1.73.0_1
Size to download: 136MB
Size required on disk: 509MB
Space available on disk: never enough!
Cheacking Debian and NetBSD it seems like the Rust standard library is smaller, much less than 500MB.The simple question is how much space is required for an installation of the Rust compiler. On VoidLinux, it's significantly more space than for a GCC installation. 509MB is just too much for my tastes.
Had this experience in my first Rust program. It turned out Rust had silently changed the types of some variables based on some code hundreds of lines later, which caused an overflow at runtime. Spooky action at a distance!
I'm curious if this is other's experience as well. I've only dabbled in Rust but it isn't clear to me how it would ensure that the code is correct.
No. This is an exceedingly rational anger.
But maybe I’m bikeshedding by saying that.
This made me not want to try Rust. I was coding vanilla JavaScript for a very long time before trying TypeScript but when I did I was amazed at how much easier refactoring became. That alone made me very excited about type systems and I instantly started looking at Rust, thinking it would be even better. Apparently not. Maybe the sweet spot (for me) is TypeScript.
This is a pretty negative and unfortunate take on rust that I cannot recognize after having spent the past 2 years professionally writing backend rust. I did write c++ before that so it's likely that it takes some experience with the pain of an unhelpful (and in some cases downright hostile) to fully understand and appreciate the so called "yelling" rust does.
From my perspective every lifetime complaint the compiler has is a deeply appreciated hint that not only saves you countless hours of debugging down the line but also makes me feel confident/safe in the code which you never could achieve in a c++ codebase without making defensive copies everywhere.
Being able to lend/borrow data without fear truely is rust's greatest strength and something people coming from GC languages have a hard time appreciating.
Meh, the fact that Rust's compiler is overly strict is just a truth (All compiling programs are valid, but not all valid programs are compiling), and I find it lowkey annoying that every time someone has a problem with it, rustceans jump to the defense to the point of almost denying the OPs experience. I mean on a case by case basis it might simply be a difference of experiences and both are valid, but on a larger scale it does look like a bit of a pattern, at least to me.
Ofcourse, there restrictions aren't just a whim of a Rust core team (usually ;p ), and do come from practical limitations but regardless, it's fair to be frustrated at them.
Was that the result of a process in which the compiler “abused” me? I don’t think so.
The author also wrote >[Rust's] dev tooling leaves much to be desired
I can't even begin to comprehend such a statement. Rust's tooling ecosystem is the number one reason why I want to convince my colleagues to give it a shot. Its literal heaven coming from C++ with cmake...
I need to use ChatGPT as a programming sidekick/pair programmer/mentor. Yes, I am not afraid to admit - I need an AI assistant and won't program without it - AI is far too valuable.
If ChatGPT struggles with a programming language that is a big problem.
I have had a great experience with ChatGPT with TypeScript, SQL, Python, golang, C++.
I tried ChatGPT with Rust and it essentially failed - it does not understand the language.
AI has become an essential tool for me in programming if the language doesn't do AI or doesn't do it well then that's a huge mark against it.
Plus, I'm way more trusting of a Stack Overflow post or even a blog post than what AI generates. I mean, AI hallucinates all the time or generates things that are subtly wrong, so unless you can write the code yourself how can you trust the generated stuff?
Pasting in an error message, made chatgpt spit out another version of the code with some other error.