This was something that surprised me about the Rust ecosystem, coming from Go. Even a mature Go project (e.g. some business' production web backend) may only have 10-20 dependencies including the transitive ones.
As noted in this post, even a small Rust project will likely have many more than that, and it's virtually guaranteed if you're doing async stuff.
No idea how much of it is cultural versus based on the language features, e.g. in Go interfaces are implicitly satisfied, no need to import anything to say you implement it.
I hate that. I don't want dependencies for serialization or logging. But you do and now you have to choose which of the dozen logging crates you need.
As a beginner this is horrible, because everybody knows serde, but I have to learn that serde is the defacto, and that is not easy because when coming from other languages, it sounds like the second best choice. And that is with most rust crates.
Which may or may not be fine in a Go binary that runs on a modern desktop CPU, but what if your code is supposed to run on say an ESP32-C3 with a whopping 160 MHz RISC-V core, 400 KB of RAM and maybe 2 MB of XIP flash storage?
You could of course argue that that's why no-std exists in Rust, or that your compiler might optimize out the animated GIF routines, but personally, I'd argue that in this context, it is bloat, that - while it could occasionally be useful - it could just as easily be a third party library.
It’s the same as in C, Rust, or any other programming language I’ve ever used. If you don’t use a library, it doesn’t end up linked in your executable. Don’t want to animate GIFs on your microcontroller, then you don’t write `import “image/gif”` in your source file.
I think the lack of strong standard library actually leads to more bloat in your program in the long run. Bloat is needing to deal with an ecosystem that has 4 competing packages for time, ending up with all 4 installed because other libraries you need didn’t agree, and then you need ancillary compatibility packages for converting between the different time packages.
> It’s the same as in C, Rust, or any other programming language I’ve ever used. If you don’t use a library, it doesn’t end up linked in your executable.
I don't think that's true. If the standard library is pre-compiled, and it doesn't use `-ffunction-sections` etc. then I'm pretty sure you'll just get the whole thing.
There is experimental support for building Rust's standard library from source, but by default it is pre-compiled.
It's hardly the case that a good reason to not have a more complete standard library on the basis of having to do a tiny bit more work in a more special case to get binary sizes down.
Does anyone use the full standard library for embedded targets? I've not seen it done in C, java has a special embedded edition, python has micro-python, rust seems to usually use no-std, but I might be wrong there.
It seems like a bad reason to constrain the regular standard library
Agreed, and the small stdlib is one of the main reasons for this problem. I understand the reasoning why it's small, but I wish more people would acknowledge the (IMHO at least) rather large downside this brings, rather than just painting it as a strictly positive thing. The pain of dealing with a huge tree of dependencies, all of different qualities and moving at different trajectories, is very real. I've spent a large part of the last couple of days fighting exactly this in a Rust codebase, which is hugely frustrating.
(I've been on libs-api, and libs before that, for 10 years now.)
API Stability. When the standard library APIs were initially designed, "the Python standard library is where packages go to die" was very much on our minds. We specifically saw ourselves as enabled to have a small standard library because of tooling like Cargo.
There are no plans for Rust 2.0. So any change we merge into std is, effectively, something we have to live with for approximately forever. (With some pedantic exceptions over edition boundaries that don't change my overall point.)
Nuance is nearly dead on the Internet, but I'll say that I think designing robust and lasting APIs is a lot harder in Rust than it is in Go. Rust has a lot more expressiveness (which I do not cite as an unmitigated good), and the culture is more heavily focused on zero-overhead abstractions (which I similarly do not cite as an unmitigated good). That means the "right" API can be very difficult to find without evolution via breaking changes. But the standard library cannot, generally speaking, make breaking changes. Crates can.
I would suggest not reading the OP as a black-and-white position. But rather, a plea to change how we balance the pros and cons of dependencies in the Rust ecosystem.
I can understand not wanting to add SMTP or CGI to the stdlib. But a lot of common POSIX functionality (which is sometimes a single syscall away) is missing too.
A lot of common POSIX functionality is not missing though. I was able to write ripgrep, for example, by almost entirely sticking to the standard library. (I think the only place I reach out to `libc` directly is to get the hostname of the current system for rendering hyperlinks in your terminal.)
We also came at the standard library with a cross platform mentality that included non-POSIX platforms like Windows. You want to be careful not to design APIs that are too specific to POSIX. So it falls under the same reasoning I explained above: everything that goes into std is treated as if it will be there forever. So when we add things to std, we absolutely consider whether the risk of us getting the API wrong is outweighed by the benefit of the API being in std in the first place. And we absolutely factor "the ease of using crates via Cargo" into this calculus.
I peeked at the code for gethostname in ripgrep, and it's nice and straightforward.
Much like op said here; we have a culture of "don't write unsafe code under any circumstance", and we then pull in a dependency tree for a single function that's relatively safe to contain. It solves the problem quickly, but at a higher price.
BTW, thanks for ripgrep. I don't actually use it, but I've read through different portions of the code over recent months and it's some very clean and easy to understand code. Definitely a good influence.
I don't think you should treat unsafe code as that level of toxic. It's necessary when interfacing with with system APIs. The important part is that you try to have safe wrappers around the unsafe calls and that you document why the way you're using them is safe.
In addition to the points burntsushi gave in the sibling comment, I'd also add that keeping the standard library small and putting other well-scoped stuff like tegex, rand, etc. in dependencies also can reduce the burden of releases a lot. If some a bug gets found in a library that's not std, a new release can get pushed out pretty quickly. If a bug gets found in std, an entire new toolchain version needs to be published. That's not to say that this wouldn't be done for critical bugs, but when Rust already has releases on a six-week cadence, it's not crazy to try to reduce the need for additional releases on top of that.
This probably isn't as important as the stability concerns, but I think it still helps tilt the argument in favor of a small std at least a little.
One risk with a bigger standard library is that you'll do an imperfect job of it, then you'll be stuck maintaining it forever for compatibility reasons.
For example, Java developers can choose to represent time with Unix milliseconds, java.util.Date, java.util.Calendar, Joda-Time or java.time.Instant
It’s really just Date and Instant. Joda-Time isn’t part of the standard library. And if you’re listing Calendar, you might as well also list ZonedDateTime, OffsetDateTime, and LocalDateTime, not to mention stuff like java.sql.Date.
In reality, there’s just one old API and one new API, similar to the old collection classes (HashTable, Vector, etc.) and the newer JCF ones.
- backward compatbility: a big std lib increases the risk of incompatible changes and the cost of long term support
- pushing developers to be mindful of minimal systems: a sort of unrelated example is how a lot of node library use the 'fs' module just because it is there creating a huge pain point for browser bundling. If the stdlib did not have a fs module this would happen a lot less
- a desire to let the community work it out and decide the best API/implementations before blessing a specific library as Standard.
In my opinion a dynamic set of curated library with significantly shorted backward compatibility guarantees is the best of both worlds.
- less burden on the stdlib maintainers (which are already overworked!)
- faster iteration on those libraries, since you don't need to wait a new release of the compiler to get updates for those libraries (which would take at least 12-16 weeks depending on when the PR is merged)
IMO they should over time fold whatever ends up being the de-facto choice for things into the standard library. Otherwise this will forever be a barrier to entry, and a constant churn as ever new fashionable libraries to do the same basic thing pops up.
You don't need a dozen regex libraries, you just need one that's stable, widely used and likely to remain so.
> You don't need a dozen regex libraries, you just need one that's stable, widely used and likely to remain so.
That is the case today. Virtually everyone uses `regex`.
There are others, like `fancy-regex`. But those would still exist even if `regex` was in std. But then actually it would suck, because then `fancy-regex` can't share dependencies with `regex`, which it does today. And because of that, you get a much smoother migration experience where you know that if your regexes are valid with `regex`, they'll work the same way in `fancy-regex`.
A better example might be datetime handling, of which there are now 3 general purpose libraries one can reasonably choose. But it would have been an unmitigated disaster if we (I am on libs-api) had just added the first datetime library to std that arose in the ecosystem.
I would expect crates like `stdlib-terminal` and `stdlib-web-api` in that case.
Honestly, something feels off with Rust trying to advertise itself for embedded: no stdlib and encourage stack allocation, but then married to Clang (which doesn't have a good embedded target support) and have panic in the language.
Building a C++ replacement for a browser engine rewrite and building a C replacement for embedded have different and often conflicting design constraints. It seems like Rust is a C++ replacement with extra unnecessary constraints of a C replacement.
I often wonder about this: obviously Rust is fashionable, and many people push to use it everywhere. But in a ton of situations, there are modern memory-safe languages (Go, Swift, Kotlin, Scala, Java, ...) that are better suited.
To me Rust is good when you need the performance (e.g. computer vision) and when you don't want a garbage collector (e.g. embedded). So really, a replacement for C/C++. Even though it takes time because C/C++ have a ton of libraries that may not have been ported to Rust (yet).
Anyway, I guess my point is that Rust should focus on the problem it solves. Sometimes I feel like people try to make it sound like a competitor to those other memory-safe languages and... even though I like Rust as a language, it's much easier to write Go, Swift or Kotlin than Rust (IMHO).
> it's much easier to write Go, Swift or Kotlin than Rust (IMHO).
While I respect your opinion, I must say I find it much more difficult to get work done in golang than rust. Mainly due to stringly typed errors and nonexistent documentation culture. Trying to figure out what error states my program might get itself into is essentially a futile exercise in golang whereas in rust it's generally annotated right there in the source code by lsp.
Exactly this. You want logging in Rust? You will need at least `log` and another logger crate, for example `env_logger`, maybe the `dotenvy` crate to read `.env` files automatically, you already have 3 direct dependencies + all the transitive ones.
As noted in this post, even a small Rust project will likely have many more than that, and it's virtually guaranteed if you're doing async stuff.
No idea how much of it is cultural versus based on the language features, e.g. in Go interfaces are implicitly satisfied, no need to import anything to say you implement it.