> If you do need generic interface for better type-safety and ergonomics, make sure that the interface layer is thin, and that it immediately delegates to a non-generic implementation. The classical example to internalize here are various functions from str::fs module which operate on paths:
> But the Rust compiler should be doing this automatically
This will become a lot easier once the actual full const-generics implementation lands. Because then even your "non-generic" implementations will be enabled to depend on compile-time-defined features of the instantiated type (such as size, alignment, value bounds etc.) without any sort of duplicate code generation. Of course, these in turn should delegate to truly non-generic code when feasible to minimize bloat.
Regular jack-off here. It’s hard enough coding in Rust as-is. In addition to that, I don’t think I could take into consideration a coding style just to bring the build times down.
As a side note, looking at this block of code, the syntax is just so ugly and overcrowded. I don't get how a relatively new language ended up looking so messy so fast; it's like no one cared about readability.
Partly personal preference in both what is readable and in how much you optimize caller convenience vs simple callee code. I think it's fine to just expect the caller to do the conversion. In fact, it's often automatic. (Eg if you have a PathBuf, then just sticking a & in front of it is sufficient to supply &Path via Deref<Target = Path>.) So I'd write this function as follows:
fn read(path: &Path) -> io::Result<Vec<u8>> {
let mut file = File::open(path)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
which I find readable. Comparing to another language...I haven't written Go in a while, but I think the rough equivalent would be:
(Maybe in this case you'd just return the io.ReadAll rather than doing an if, but I'm trying to use similar constructs. The ? is something you can use all throughout the function, not just at the end, so I tried to match that pattern.)
[edited: I said there wasn't a io.ReadToEnd extracted from io.ReadFile in Go, but jatone pointed out it's called io.ReadAll.]
It's so that a reader of the calling code can immediately reason about which calls might be mutating state. If you have 10 calls, all taking &bytes, any of which may or may not be mutating the data while others are merely reading it, it becomes very difficult to reason about.
By being explicit about this, Rust becomes easier to read, but (slightly) harder to write.
Shared borrows and unique borrows also behave very differently in Rust. '&mut' for unique borrows is a bit of a misnomer, because passing some objects via a shared '&' reference might also mutate state 'indirectly' via internal mutability.
> So why force every caller to do a `&mut` un-necessarily ?
Because it's not unnecessary? `bytes` is a `Vec`, not a `&mut Vec`, those are different things, and Rust generally avoids magically doing things under the covers. So it doesn't auto-ref (let alone auto-mut-ref) parameters, only subjects.
Method syntax is reserved for functions. There are no method syntax macros. Since a return in a function terminates execution of this function, and not its caller, your example would not work.
Right, so this should really be .orReturnErr , by analogy with .await . We just overload the dot symbol to mean both "method call" and "weird monadic stuff is happening here".
These differences are mostly superficial. We all need to decide if we're going to avoid a language because its punctuation choices aren't "good enough" for us or if we're going to choose the best tools for building useful systems.
"orReturnErr()" seems quite against the Rust philosophy though — refusing the method call syntax for early return means folks can easily miss something important. Rust uses ? for error handling, ! for macros, and keywords (with no parenthesis) for other control flow. I prefer it that way.
I don't know Rust but converting that code snippet of Go code you wrote to (non-idiomatic [wouldn't use upper-case characters, namespaces used doesn't actually exists]) Clojure:
Unsurprisingly APIs can be more readable when you don't ponder why they're designed the way they are and completely ignore their purpose.
The `read_to_end` API[0] was designed such that the buffer could be reused in order to avoid unnecessary allocations.
There are helpers for common tasks, though people may not know or notice them. In the case of `Read::read_to_end`, it's called std::fs::read[1], which actually preallocates the vector based on file size internally so might be faster than hand-rolling `read_to_end`.
So here's your code in Rust, for the last 3 years or so (since 1.26 was released):
If you’re trying to actually do this in real code. The example is how you’d write this function yourself, but if you’re just trying to Get It Done, it’s significantly easier than the example.
> Your example is also less about Rust and more about type signatures.
No, is HOW Rust use type signatures. Rust is in the camp of "advanced usage", not like C#, Python and others, where a "type" is barely for classification. Instead, Rust use types to inform: Memory type (heap vs stack), Cost (cheap to copy or must clone and can be potentially very big), Permisions (read/mutable), Behavior, Share-ability (can go in threads), lazy or not (iterable), etc.
It use types for a lot, and communicate much information densely.
P.D: Not always perfect and I agree that the syntax is not as nice to me, but is incredible how much you can understand from most codebases just reading, even on docs (not need IDE assistance that much)
No, I don't really agree. The example was "def read(path)", which is intentionally vague and any typed language would clarify.
Your points can be/are valid, but they don't really change the fact that the above example was a cherry pick, and Rust's (over)use of symbols and special characters make it, again, about as hard to read as one could imagine. Just because you _can_ derive meaning from it doesn't change that.
I think that types are definitely useful in one sense, but it's not all goodies. Things like `collect` end up in return-type polymorphism teritory that require you to grok the context in a sort of deeper way than, say, with random Java code.
Yeah, the other side of the coin is idioms (like things that people understand as "pythonic" or idiomatic).
Certain idioms not make much sense at first, but a big part of be on Rust is learn them. Eventually, most of it make sense and are very predictable (is incredible how much adherence exist for them in the whole community!).
Come on, it is definitely not the case that nobody cared about readability. My sleepless nights after arguments about syntax in 2011 and 2012 very much say otherwise.
Or the endless arguments about await, or that time I cried because the discussion over what we call integers was so contentious. We all cared quite a bit about syntax. Heck that’s partially why almost none of it is novel!
I don’t think that when people say “Rust has ugly syntax” they mean that Rust has ugly syntax. It seems that what they are complaining about is actually Rust semantics, which needs extra syntax to express it.
Blog post idea: express this snippet (with references, aliasing control, generics and all) in pseudo C++, Java, Python, etc. Then express “reading a file in gc language with exceptions” in pseudo Rust.
I think rust syntax is more readable and self explains. Having syntax more minimal, more use of symbols, spaces and tab make them beautiful but they are really hard to get if it is written by someone else. I personally noticed it's more easy understand other rust project than other langs project.
I imagine it's that there's a fairly high ratio if function signatures to other lines of code, and function signatures are particularly gnarly in rust compared to many other languages, at least when lifetimes are involved.
I wonder if this is a side-effect of rust function signatures being very precise, as in the function is very clear about the allowable type and ownership of its parameters and return values, where most languages can't even express things at that level of precision so people's eyes glaze over when reading it.
I imagine so. A function signature is rust encodes more information than a lot of other languages. From my (very) limited experience, without lifetimes they look fairly similar to C++, and then adding lifetimes just makes them busier and more complex, but that's because you're encoding additional information.
I agree that I find lifetimes a bit hard to read, but outside of that I usually find Rust's syntax really clear for an algol-like. For example, they use the ML-style type declaration variable: type, which I find more readable than the C-like type variable. There's a different syntax for namespace and function chaining, which I also like.
# Your big function has specific types
function my_function(x::UInt, y::UInt)
[ ... ]
end
# Wrapper function to minimize monomorph. cost
my_function(x::Integer, y::Integer) = my_function(UInt(x), UInt(y))
Rust need a lot of extra stuff in function signatures to do its magic. Julia couldn't possibly do what Rust does because it doesn't have enough information
No. They didn't have to have a pub keyword, which takes space. They didn't have to have the ugly syntax to make functions generic: Julia's functions are generic without using those brackets. Rust didn't have to have explicitly needed return types, or having to have those fully specified.
So how do you make the difference between public and private code without a pub keyword? And for the "explicit return types", I think it makes the code more readable. You seem to think that what is your personal preference (something that looks like Julia) is best practice. It's not, it's just a preference.
Both Julia and Python has a concept of public and private code, neither have a pub keyword.
I don't believe Julia or Python is "best practice" over Rust. I even empathize with the opinion that explicitness may be preferential sometimes. But you _must_ be able to see how Rust's syntax overload obfuscates the core business logic, right?
The difference between pub in Rust and _/__ in Python isn't that big. It's like Go that uses a capital letter to say what code is public. There's also a difference in philosophy between public by default and private by default. All in all, it's a small detail and doesn't affect things.
Sure, business logic is more readable in something like OCaml than in Rust. On the other hand, Rust isn't really worse than something like Java, and I like having more information than what you have in Python. I'll also add that Rust is often chosen for speed and reliability, and that part of that syntax is here to guarantee that. It's not directly business logic in the traditional sense, but it's something you expect from your code.
All in all, Rust is still not at the level of the "platonic ideal for syntaxes", especially not for most of the code I write (business logic for web apps, which wouldn't need Rust's speed). Still, in the realms of programming languages that are used, it's one of the best.
> The difference between pub in Rust and _/__ in Python isn't that big.
FWIW `__` doesn't mean "this is part of the implementation", it means "I'm designing a class for inheritance and I don't want child classes to stop on my attributes".
> Both Julia and Python has a concept of public and private code, neither have a pub keyword.
No. Python certainly does not have a concept of private code[0]. The Python community has a suggestion of things being implementation details.
Surely you can see that absolutely would not be good enough for a language which strives for static correctness? And even more so as the soundness of `unsafe` blocks often relies on the invariants of the type it's used in?
[0] well that's not entirely true, I guess there's native code.
Fully specified function signatures are a conscious decision because it allows you to type check and borrow check a piece of code without needing to analyze the bodies of other functions. I also love it as a human because there is no guesswork involved.
Rust generics can be written in a more compact way,
fn f(x: impl Test) { ... }
Is basically the same as
fn f<T: Test>(x: T) { ... }
Having to annotate the return value of functions is partly to make sure don't break semver accidentally, but it also constrains type inference to a local search rather than global search, avoiding spooky action at distance (when modifying a function changes the signature of another function). It seems that semver hazards are more important in languages with static types.
That pub? It's punching waaay above its weight! Having things private by default protects against semver hazards too, but it's also the major tool that enable writing safe code that uses unsafe { .. } blocks underneath. That's because a single unsafe block typically taints the whole module it's contained (such that even safe code needs a safety review). Being able to write safe abstractions of unsafe code is the pull of Rust so people need to get it right.
For example, setting the field len of a Vec to a value larger than what's initialized in its buffer will cause UB when accessing that memory; if this field weren't private, making a safe Vec would be impossible (likewise, a function that could set the len to any value must be private, etc).
So pub can only be used for things that are safe to export, and people should be careful with that.
Here's my least favorite syntax from there: that damn Ok(..). Ok-wrapping sucks. There were proposals to remove it, but they were rejected. There's a library that removes it [0], and people should use it. It also makes the Result<..> return type implicit in the function signature too.
Those two are related, clearly. It's not that Rust has added words that literally do nothing. Instead, the designers of Rust packed all kinds of things into the syntax that other languages decided would cause too much noise.
* The output type must be explicitly named instead of inferred
* The let keyword for creation of variables instead of merely assignment
* The mut keyword to distinguish between mutable/immutable variables
* The pub keyword, instead of another mechanism
* Semicolons needed after every expression
And more. Again, for every one of them, you can argue that it's _good_ for it to be there, because it serves a function. But you end up with a language with a hugely bloated syntax where the business logic is sort of swamped in these accessory keywords.
> The output type must be explicitly named instead of inferred
That isn't syntax. It's a decision about analysis.
> The pub keyword, instead of another mechanism
The "other mechanisms" discussed are in fact not providing this feature, but pretending you needn't care, and I'd argue Rust shows that in fact you should care and pretending otherwise is a problem.
> Semicolons needed after every expression
On the contrary, semicolons are at the end of Rust's statements. If what you've got is an expression you don't need a semicolon. For example here's how log is defined on an f64 (a double precision floating point number)
I don't agree that the syntax is "hugely bloated". The return type is part of the business logic. mut and pub are here to make some things explicit, so again I don't mind them. Rust is one of those languages that wants to make more things explicit because the people using it need those things to be explicit.
The worst decision regarding Rust syntax was following C++ syntax. Due to this, we write, in type syntax
Things<Like<This, That>, That> -- as in C++
Instead of a more legible (and very familiar to Rust authors)
Things (Like This That) That -- as in Haskell
Inherited from C++, we had also :: for namespacing (while a dot . could be perfectly be used for that), and tons of other stuff like <> for scoping generics.
Perhaps it looks a little better? It looks for me, at least.
For doing better than that we could drop C-like syntax perhaps?
The trouble is that languages have a weirdness budget - there's a finite amount of novel stuff a language can have until it becomes too weird for mainstream programmers. So language designers have to pick their weird features carefully. Spending it all in novel syntax is unwise.
I think that at the time the Rust pull was safety for low level code and, as Rust was targeted to replace some C++ components, following C++ syntax was seen as a good idea.
What’s the worst decision for you is my favorite feature of the language syntax for me. I find it easy to read and very easy to navigate in the text editor because types are enclosed by balanced operators that editors can easily highlight and jump between.
> Rust was targeted to replace some C++ components, following C++ syntax was seen as a good idea.
And that was a good decision. If it were too far from the C++ syntax, I would not have learned it, because as a mostly C++ guy at the time, some similarities with C++ helped me keep interest in Rust. Also, I was unable to parse Haskell's "type parameters as function applications" syntax, which was purely confusing. The Rust syntax is an abomination, that's true, but it is a necessary evil.
By comparison, near-source-compatibility with C is arguably the "worst" design decision that C++ made, a constant source of complexity and frustration, and yet the #1 reason behind C++'s early growth and continued success. There are a lot of different factors at play here.
Dot syntax is used when accessing a function/property on a value, :: is used when accessing a function/property on a module or type. I think it's great having this visual distinction, especially given that you can call the same function in either style:
Function application and type annotation both using parens is pretty jarring for a lot of people. I much preferred the proposal that would have used [] like typescript or scala.
I think the language team has done a great job. Rust may look a little ugly on the surface if you're just getting into it, but I'm super thankful they opted to require functions to always spell out all type signatures. Generics can get a little overwhelming but like any other skill, it just takes a few hours/days of practice to learn how to read them.
One sad aspect is the ? operator. In the beginning, Rust thought it could get away with explicitly handled return codes, without having support for direct style/exceptional error handling. But they've ended up with that in the end. If Rust had just used checked exceptions from the start (or equivalently if they had do-notation) they wouldn't have this lingering syntactic noise. Hindsight is 20/20 though - it would have been hard to convincingly predict that Rust would eventually end up with checked-exceptions-plus-syntactic-noise as its error handling paradigm - the experiment had to be carried out to see that it would end up like this. (Now, of course, exceptions have been thoroughly vindicated, and it would be silly to design a new language with explicitly handled return codes instead of checked exceptions)
You are completely wrong about ? in my opinion. Between it, built-in load carrying tagged enums (also known as sum types) and the built-in Result and Option types, they essentially solve the exception problem while still being entirely explicit. It's probably my favourite feature of Rust.
? is not "syntactic noise". Having an indication in the code of which operations might throw is very useful, and it enables a further simplification of using ordinary variant records as opposed to a special-cased "checked exception" construct.
Compare it to unsafe functions. An unsafe function can be called in an unsafe block or an unsafe function. Rust could require annotating each call to an unsafe function, even inside unsafe functions. Why don't we do that? That seems like it's also very useful for the exact same reasons.
A proper language needs to allow the programmer to choose between implicit effects and explicit effects on a case-by-case basis. The reason ? is so tragic is because Rust tried to go with explicit effects only, but found that people wanted implicit effects, and ? is the closest that Rust can get. That's why it's syntactic noise: The programmer wants to get rid of it, they don't regard it as informative for this case, but they can't get rid of it.
(And checked exceptions can be perfectly well implemented with regular data, you don't need any special cases - in fact, you can do it with the same infrastructure that's already been added in a special-cased form for async/await support...)
Didn't downvote you because I presume you are arguing in good faith, but I have very different opinion about the ? operator, it is one of the best syntactic things in Rust. In my experience, many Rustaceans agree. It's a minimal nuisance, while still being explicit in a good way.
Also, being able to call unsafe code inside unsafe functions is now considered a design mistake, and there is ongoing work to revert the default – requiring unsafe blocks even in unsafe functions: https://rust-lang.github.io/rfcs/2585-unsafe-block-in-unsafe...
The compiler does have a lint to force being explicit about unsafe in the way you describe. It is an allow by default lint called `unsafe_op_in_unsafe_fn`.
It is what usually happens when you design by committee instead of everyone having a common understanding and working towards the same goal under a single person/entity.
Not saying it's good or bad, just how Rust ended up where it is.
>[Build time] also is a non-linear factor. Just waiting for the compiler is the smaller problem. The big one is losing the state of the flow or (worse) mental context switch to do something else while the code is compiling. One minute of work for the compiler wastes more than one minute of work for the human.
This is so true, and so hard to convey to a lot of people, even in engineering. They like to abstract humans as machines that can just context switch at a whim, which is just not true.
I'm surprised nobody has mentioned trying out using Google's Bazel build system with the Rust rules. Bazel provides relatively straightforward 'remote caching' of build artifacts via Google Cloud Storage or S3.
Another few benefits with Bazel is that your build is mostly hermetic and reproducible. I've never had to run `bazel clean` and the output sha of your builds will always be the same (good for signing and security). You can also use remote build execution (RBE) to run your builds in parallel. This makes for a massive performance boost when you're compiling a lot of libraries (packages, source files, etc).
It also models your source code in your build system (exposing libraries, binaries, etc).
I'd love to see a performance comparison with `rules_rust`.
Of course, making the right abstractions and library / module / crate boundaries is still very much important to incremental build performance. For 'very large' Rust projects, setting up remote build execution can be hard, but a few of the original Bazel folks at Google are trying to make that easier for folks with EngFlow https://www.engflow.com/
Bazel's learning curve is really high if you're learning it from the scratch from the web. It took me most of a week to figure out how to write a custom rule that just calls a python script that dumps out a tree of files. What would been a few lines of makefile turned into about a hundred lines of starlark boilerplate. I haven't given up on it yet, but you need to be using it for something serious to have any hope of amortizing the cost of porting a build to it. Rust+Bazel is combining two niches; I'm surprised there is any support at all at the intersection.
There's support because Google uses Bazel and Rust, so they need a way to use Rust from Bazel.
I haven't really used Bazel much, but the features it brings are really huge: reliable pure compilation, so you can actually do globally cached and distributed builds reliably.
I'm old enough that I can remember having to wait 45 minutes for a compile to finish and not be able to do anything else with the computer. The worst cases of this were the slow processes which required occasional manual intervention. Or, the situation where if there was an error during compilation, only the first error was reliably meaningful and everything after that might or might not be an accurate reflection of the state of the source code.
It's worth noting that there's a sort of uncanny valley in the amount of time something takes: when it's short, it's not too big a deal to wait for it. When it's fairly long (and predictably so), it's easy enough to context switch (I used to read a lot waiting for compiles to finish). It's the stuff in the 5–10 minute range that kills you.
What depresses me, is that while I can now do something else in a different window while a process runs, the issues with errors and requiring interaction still persist.
> Another obvious advice is to use fewer, smaller dependencies.
As a library author, I sometimes wonder if I should be introducing more feature flags into my crate to let people only use the bits that they want, in order to help compile times. The whole thing doesn't take very long to compile (~20 seconds), but maybe the added complexity of juggling feature flags might be worth it.
As someone who works on a large rust codebase with a lot of moving parts, I'd ask you to really think hard about how your use of features might affect people down the line in undesireable ways if you go down this path.
In particular, please for the love of god don't put the whole kitchen sink into the default feature. I'd even argue don't ever have a default feature at all. In practice, once a codebase gets above a specific size, it becomes completely impossible to exclude any features in a crate's default list because `default-features=false` only applies locally. Something 5 levels deep will bring those features in whether I like it or not just by not specifying anything.
Also, please don't do things like how slog uses a feature flag to control minimum log levels, or k8s-openapi uses it for version selection. These kinds of usages create extremely frustrating problems.
Honestly, features -- and especially the way default features work -- are the worst thing about cargo to me. I think they should be a lot more restricted than they currently are. If I had my way you'd only be able to use `#[cfg(feature = X)]` at module level to include or exclude code, never to select actual behaviour.
To me they're filling a hole that crates themselves should be filling. Certain language features aren't really rich enough to make really effective 'additive' crates though, unfortunately.
> In practice, once a codebase gets above a specific size, it becomes completely impossible to exclude any features in a crate's default list because `default-features=false` only applies locally
Curious, what that specific size approximately is? For rust-analyzer, getting all the deps to use features correctly is rather annoying, but isn’t particularly time-consuming.
I think we must be talking about different things because rust-analyzer doesn't enter into or help the issue I'm talking about?
If you depend on crates A and B, and B depends on A, and you specify `default-features=false` on A but B doesn't, you will always get A's default features included in your compile. There is no way to prevent this.
I'm just one voice (and I'm probably not someone who uses your specific library), but this does sound like a good idea. I'd appreciate it (though I understand that this takes effort and maintenance, so I understand if you don't want to!)
Your 20 second library is not too bad when it's used on its own, but add 5 or 6 libraries that all "only" take a few seconds and you're waiting a solid five minutes compiling other people's code and their dependencies.
Even simple Rust programs quickly end up compiling several versions of several libraries because of the dependency tree, some of which can include large libraries like Tokio and Serde. I love libraries that make use of feature flags, they massively cut down on my compile time.
This isn't really any library developers' fault, it's a problem with the entire ecosystem. Cargo is not dissimilar from NPM where you can quickly end up with an exploded dependency chain of hundreds of megabytes in supporting code before you even realize.
Doesn't this basically require buy-in across the entire crate tree? Eg even if I disable all the default features I don't need, all it takes is a single crate anywhere in the dep tree that uses the same crate with the default features to turn them on again?
forever, or until rust version update (might be often for nightly builds), or until build flag change or cargo clean, whichever happens first. It's also only a per-project target directory (not across all projects you compile).
Even for people on nightly builds of the compiler, I would be quite surprised if nearly anyone actually bothered to upgrade their compiler every night. Most people on nightly are just going to be there for the use of a specific unstable feature.
I'm convinced that most of the people complaining about build times are using docker for some incomprehensible reason and are blowing away the dependency cache with every build.
Nope. FWIW, I conpile on a MacBook. So I only have 2 cores, and laptop ones at that. Perhaps that's affecting our difference in perspective?
I also had a project a couple of years ago where I had to compile it in release mode to get reasonable performance for testing (Iw as processing GB's of data), which meant 2-3min incremental compile times. Not fun!
This depends on what the library does, of course. From a quick Google I believe the parent has made several packages, like a front-end to help with XML trees or a full wrapper around procfs. Libraries that complex (especially with their dependencies) can take significant time to compile, it's just a consequence of their complexity.
Some smaller libraries I've seen that just do some annoying-to-test but simple string manipulations have had similar compile times, mostly because they include specific versions of advanced logging or error reporting libraries for no obvious reason.
For example in the `procfs` crate, there's a dependency on `flate2` just in case a /boot/config.gz file exists (in order to see what options the kernel was built with). But if a consumer never needs to inspect the kernel options, this dependency (and related code) is just dead-weight.
The nice thing about rust is that this code will be removed from the final executable if it's unused, but maybe it's time to start thinking more carefully about compile times.
It all depends on how many times you do it! 20 seconds for an edit-compile loop is huge. 20 seconds for a CI build is not as bad, but still bad (if you have dozens more).
20 seconds once per machine per dependency version (i.e. there is no rebuild of the dependency until you change compiler flags or update the library) is OK.
Fwiw unless the flags really represent independent bits of functionality, I find many features more of a pain to deal with than dependency compile times, given that even in our big workspace, a combination of incremental builds and sccache makes compile times mostly a non issue.
Yeah, the position you articulated is my main concern. Do you think it makes any differences if the features are enabled-by-default? If so, the "default" experience isn't any different than it is today, but discerning consumers can disable things if they need it. But also, an "everything-by-default" approach doesn't seem that helpful to the global ecosystem.
Yeah I think default features resolve a fair bit of the pain. I wonder though how many consumers are actually going to go through the hassle of slicing just what they need out of a library. For something as large and featureful as Tokio it makes sense, or for optional additions to core functionality, but we at least haven’t yet found ourselves going around and looking for places to trim the fat. One dependency like diesel or something similar eats up easily 10x the compile time of any other single dependency, so we’ve been more focused on rooting out those big offenders when we can. I definitely wouldn’t want to ask that OSS maintainers take on extra maintenance burden unless there’s a good reason.
Features are super neat, but it’s easy to see them overused. It’s also a bit hard to adequately test arbitrary combinations of features. It’s a different concern from a library, but we saw this with one of our applications. We had a situation where we were using features where we should have just been using env vars for feature flagging, and it caused a fair bit of hassle. We’ve since ripped out that usage, because we never in practice tested or deployed with anything other than “all features,” and even in the case where we want to use some functionality to upsell, enabling an env var and restarting is way easier than recompiling the project.
Personally, dynamically linking dependencies and using the `mold` linker has given me the most bang for my bucks. You can't dynamically compile all crates (actix_web, sqlx) unfortunately due to some assumptions macros make about crate paths (using Bevy scheme). And this exclusion extends to all crates pulled in as [dev-dependencies].
I tried again to use rust for something the other day. Just wanted to fetch some things via http. Nothing fancy, didn't even really care about the response body, just the timings.
Consensus seemed to be that reqwest is the way to do this using rust, since the stdlib does not include an http client.
Adding the dependency on reqwest tried to pull in over 100 dependencies and after building for a while, it failed somewhere along the way because my rust version was too old. Now, maybe some of these were optional dependencies, but they all got pulled in automatically.
so, when TFA says
> Another obvious advice is to use fewer, smaller dependencies.
I just went back to go, which can do the crazy obscure task of making an http request without requiring 100 external dependencies.
Those dependencies aren't recompiled every time you change something. They will only impact your initial build or a clean build on a CI/CD system.
Raw dependency counts can be shocking for those coming from other languages where pulling in a dependency is an uncommon operation, but it's not a bad thing. I'd rather have my HTTP client library re-use popular, tested packages for things like decoding JSON, handling TLS, working with IP addresses, and other common tasks. I don't think it would be better to have every package roll their own versions of all of those common pieces.
Notably, several of the big dependencies are maintained by the same person who maintains reqwest. It's a common practice with Rust crates to split a project into reusable parts. Reqwest uses hyper (http client), http, http-body, mime, url, and several other crates that are owned by the person who owns the reqwest crate.
If we instead mashed all of those dependencies together into a single crate it would appease the people who don't like seeing high dependency numbers, but it wouldn't really make the product any better. It would make it harder to re-use the individual pieces, though.
> I just went back to go, which can do the crazy obscure task of making an http request without requiring 100 external dependencies.
Go is famous for including HTTP utilities in the standard library. If I was building a simple app that made HTTP requests and little more, I'd probably pick Go as well. Or Python.
Or you could have searched for a minimal HTTP request crate ( https://github.com/algesten/ureq ) designed to be small instead of using the most full-featured crate.
If your goal is to reach for something quick and easy with batteries included and minimal external dependencies, Rust is not a good choice. Doesn't mean Rust is "bad", but it's not the right tool for every simple job. You also can't just pick the most popular crate and assume it's exactly what you want.
> It's a common practice with Rust crates to split a project into reusable parts.
Precisely this. Rust is not the same as Go or Java or Python: it's still a fairly low-level language. Standard practice is to build lower-level components (like http parsing and url crates) that are abstracted over by more 'friendly' crates (like reqwest). It's jarring, and certainly different from a lot of other languages, but it's a testament to the language's flexibility in my eyes.
An other factor is that the unit of building / concurrency in compilation is the crate. So splitting a crate into 15 sub-crates (which could be reused independently) increases parallelism.
Funny thing is, Rust and JS are both given that comment.
Seeing how they are different languages, there's not a single language feature that explains this dependency gluttony.
I'd like to open up the option that it is a necessary side-effect of large ecosystems: more devs using the language, more packages published, more quality in small packages. And then, when building a large-ish package, it only makes sense to use existing packages rather than reinventing the wheel.
And so, driven by nothing but good will and best practices, since you have a lot of prebuilt "assets" at your disposal ,your project ends up with 100s of deps and everybody complains.
> when building a large-ish package, it only makes sense to use existing packages rather than reinventing the wheel.
Yes, for instance one of `reqwest`'s dependencies is `base64`. We can argue both that re-inventing base64 in every big library is pointless, and also that a language doesn't need base64 in its stdlib because C and C++ don't have it.
There's no right way to cut this cake - Many a language has included something in the standard library, and then struggled to fix a broken interface once everything depended on it. (Like JS picking UTF-16 for strings) Many a language has also failed to include something in the stdlib and caused every application to suffer and re-invent it. (Like C++ not really saying anything about Unicode strings, which means Qt and everyone else has their own little take on it.)
And the suffering is only bigger if you make dependencies artificially hard to add.
Rust's approach is more flexible, at the cost of building the universe from scratch every time you make a new workspace. Personally I don't mind it. At least it's not like the problems I've heard from Haskell land. Moving from futures to async in Rust was fairly smooth.
Yeah in the debate about dependencies, people often frame it as more dependencies == bad and less dependencies == good. Except more/less dependencies is actually more or less explicit dependencies. Fact of the matter is that you need a crazy amount of dependencies to write most modern software. You need ways to handle Unicode, to connect to databases, to write async code, to handle dates, etc.
The question is whether that code is going to come from a monolithic Swiss Army Knife of a library (aka a standard library), a bunch of packages, or some code that everybody copies into their codebase. In which case the packages option doesn't seem so bad. At least they're explicitly versioned, tracked for vulnerabilities and can be updated at a granular level.
Because when working in large corporations one ends up having to write license checker tools and having humans go through those packages, alongside legal, to validate what is allowed to appear on the final product, and to vendor them on the internal package repository.
The process for proposing new modules for Rust's stdlib is pretty streamlined. Small API additions can be proposed directly via PR to rust-lang/rust, whereas large API additions can be brought up for discussion in rust-lang/rfcs. If anybody thinks that base64 deserves to be in Rust's stdlib, I encourage them to propose it (to my knowledge nobody ever has).
You're nitpicking one example. Putting base64 into std makes the higher level comment go from complaining about 100 dependencies to complaining about 99 dependencies.
I don't have an opinion about the broader dependency problem in Rust. The comment to which I responded was hosting an internal debate about Base64 and UTF-8. I think that debate is easy to resolve: use UTF-8, put Base64 in the stdlib. Do you disagree?
I guess I'd just say that the aggregation of these "keep base64 out of std" decisions are why every Rust thingy I put together ends up with a zillion dependencies. There might be an upside to that (I have not detected it), but there's an obvious downside.
Rust gets a bunch of stuff right that other languages missed. I don't think this one of them. I think Go's strategy of having a pretty-ok version of most of the table-stakes things developers do is probably the right one, just like I think sum types and option/result are better than error types and `nil`.
I share your irritation at people who drop into the middle of discussions like these to nitpick random language war arguments, but this is a thread that is actually about Rust dependency hell, and the comment I responded to, I think, sort of diagnoses why Rust has that "problem" (if you think it's a problem --- I do, but whatever).
For whatever this is worth, by the way, languages that have Base64 in std appear to include Ruby, Javascript, Python, Clojure, Java, Scala, Go, Elixir, Nim, and Swift. OCaml, C++ and Haskell do not.
> I guess I'd just say that the aggregation of these "keep base64 out of std" decisions are why every Rust thingy I put together ends up with a zillion dependencies.
Right, I partially agree, and this is a more substantive critique. But it's a much harder position to put into a pithy HN comment that sounds obviously correct. ;-)
With that said, it's pretty common in my experience to wind up in exactly the same sort of situation in Go-land. Go's standard library might hold it off for a little bit, but I seem to accrue large dependency trees there too.
> There might be an upside to that (I have not detected it)
Aw c'mon. The comment you were originally nit-picking even called it out. I'm sure you've heard the quip, "the standard library is where things go to die." And Python's in particular is often brought up as an example. Go's standard library hasn't suffered as badly as Python's has, but there are parts that have died.
If you've used Python a lot, then the fact that you have to use 'requests' and not std's HTTP client is probably not a downside. You've already digested that ecosystem knowledge. But as stewards of the standard library, this is something we see as a downside.
> For whatever this is worth, by the way, languages that have Base64 in std appear to include Ruby, Javascript, Python, Clojure, Java, Scala, Go, Elixir, Nim, and Swift. OCaml, C++ and Haskell do not.
Yes. Our design philosophy around std learns from some of those languages. Python in particular has influence in two respects. First is what I said above: when you have to make an HTTP request in Python, the standard advice is "use the third party requests library" despite the standard library having an HTTP client. This should make it clear that following your advice is merely a necessary criterion; it is not sufficient. Second is that "std should remain backward compatible for approximately forever." That means we have a very high bar for including things. (I am saying "we" here because I am on the Rust team that decides these things.) API evolution is more difficult in std then outside std, because we can't just release a 2.0. So if we get the API wrong, it sucks. And eventually a superior third party crate appears and everyone has to "learn" to use the third party crate instead of std.
And of course, adding things to std also means a higher maintenance burden. There have to be people willing to champion and maintain these sorts of huge additions. And many of us (now speaking as the regex crate maintainer) are actively opposed to throwing it into std precisely because it becomes more difficult to evolve.
Even if we did all this and put whatever pet thing it is you want to use without pulling in dependencies (I guess that's "HTTP client" here), that doesn't really solve the general problem. Eventually, you're going to pull in a dependency for _something_. And that crate's author may have decided that depending on something else instead of rolling their own thing was the better choice. Or also the opposite: perhaps they want to split apart the internals of their crate so that others can reuse those components. I did that with the regex crate. So on the one hand, I'm contributing to the high dependency count number, but on the other, I'm solving actual problems people have by providing access to internal APIs that are separately versioned. How many other regex libraries do that? Not many. Because dependencies are too annoying in the C ecosystem, which is where most regex engines live.
Getting more hand wavy... In the Go world---and I speak as someone who has used and enjoyed Go for over a decade now---performance is not a #1 concern. Not like it is in the Rust ecosystem. This has huge API implications. Culturally, people expect Rust programs to be fast and they expect to be able to achieve congruent performance as they could in C. In this environment, API complexity isn't much of a reason to not doing something. Everything gets sacrificed at the alter of performance. I know, because I've been carrying sacrificial lambs to the alter for years in the Rust ecosystem. But in Go? Nope. API simplicity is valued higher relative to things like performance.
Here's the punch line: if you want to write a program that sends an HTTP request and has a dependency count of 0, then Rust isn't the right language for you. It probably never will be.
Also, to be clear, I am someone that works both sides of this debate. I can often be found advocating against the use of dependencies and decreasing dependency count. Precisely because I perceive and experience costs associated with them. I've done work to reduce the dependency tree size of core ecosystem libraries. But there are some fundamental trade offs that put floors on how long you can go. And you can also seen me as an advocate against using many of my crates if a "simple" solution works for you. Take aho-corasick for example. My implementation of it is several thousand lines because I sacrificed just about every lamb possible at the alter of performance. But the <10 line naive implementation of multiple substring search might be just fine for your use case. And if it is, just write that and don't bring out the big guns.
Yeah, so let me be clear: I can see why, especially when the std lib was in its formative stages, the project might have decided to keep it, uh, lithe. You have Python as a very good counterexample to "kitchen sink stdlib can be good". Python's stdlib is a kitchen sink that badly needs to be cleaned out, and never will.
You're right that Go has some degrees of freedom that Rust doesn't have, especially on performance. But people do have perf problems with the Go stdlib, and they replace components (amusingly, one of the first things I built in Go did its own sockets and its own polling because Go's timers were too expensive for what I needed).
I just think it's better to have a good-enough stdlib --- with well-thought out interfaces where you're not going to have net/http and then net/http2 and then net/http3, and 16 different ways to run a subprocess and read the output --- that people can optionally outdo with 3rd party dependencies, than the situation I think Rust is in right now, where you basically can't do anything without a bunch of dependencies.
I'm definitely not dunking on Rust. We do a bunch of Rust here and I mostly enjoy working in it.
> Go's standard library hasn't suffered as badly as Python's has
In no small part because it was born 20 years later: lots of modules were added a bit haphazardly to the Python stdlib early on and the tech ended up mostly dying, but for the most part they're still there in case you care to use them. Like sunau, aiff, or dbm. Others were added because package managers were not really a thing back then so you'd try to get useful packages into the stdlib so they could be used easily, but then the maintenance burden increases a lot (and all the users assume the stdlib is maintained anyway).
Having to find, download, review, and build a bunch of extra dependencies (and surveil all the dependencies you do explicitly want to make sure they're not pulling in fucky code to do table-stakes things that the stdlib could just stdize.)
Again: these are downsides, but not dispositions. On balance there could be good reasons to have a terse, svelte, balletic std. I'm just saying, there's clearly a downside to the decision.
Without getting into Rust’s standard library inclusion philosophy, I would note that UTF-8 and Base64 are quite different cases, and shouldn’t be combined in this way.
String encoding is pervasive, and if the standard library didn’t have strings or didn’t specify the encoding, there would be much grief throughout the ecosystem, because you can’t just patch it in at the library level, it’s a language-level feature. So string encoding is a choice that has to be made.
Base64, on the other hand, is just a bunch of functions, nothing special about it that means it has to be in the standard library.
It does need to be in the standard library, because everyone needs it, almost nobody has special needs for it, and if it's not in the stdlib then you have to find, download, review, and build it and its dependencies, and that is problematic.
When I say that something “has to be in the standard library”, I mean that it can’t be implemented outside the standard library. That’s certainly not the case here. You’re using an outright bad definition of “need” here—subjective opinion rather than objective requirement.
> because everyone needs it
This is factually wildly wrong. I wrote a fair bit more here but decided it wasn’t helpful. Précis: web stuff tends to load it indirectly (though amusingly most of the time actually not use it, so that Base64 code won’t actually end up in your binary), but it’s not terribly common outside of internet stuff to reach for Base64.
I’ll leave just one more remark about Base64: once things are in the standard library, breaking changes can no longer be made; the base64 crate is still experiencing breaking changes (<https://github.com/marshallpierce/rust-base64/blob/master/RE...>, 0.12 and 0.13 were last year and 0.20 is not released), largely for performance reasons.
Please don’t just call the thin-std approach “problematic” without acknowledging that the alternative is at least as problematic, just with a different set of caveats.
> This is factually wildly wrong. I wrote a fair bit more here but decided it wasn’t helpful. Précis: web stuff tends to load it indirectly (though amusingly most of the time actually not use it, so that Base64 code won’t actually end up in your binary), but it’s not terribly common outside of internet stuff to reach for Base64.
It also seems to be distinctly losing popularity as time goes on, as the alphabet is not restricted enough to be easy for a human to sight-read/copy, and a wider alphabet will have as good an ASCII channel compatibility.
(begin rant) My issue with base64 is that + and / are terrible character for the alphabet and that the final padding = can be omitted so that base64 strings cannot be safely concatenated (as whitespace is ignored)
See also https://en.wikipedia.org/wiki/Base64#Variants_summary_table. base64url is common, with - and _ instead of + and /. As for concatenating, that’s not valid even in variants where = padding is mandatory: padding marks the end and is not allowed to occur mid-string. But I don’t think this is a problem, either: transparently concatenating Base64 strings is not generally a useful operation; I can’t think of any valid use case for it off the top of my head, I’m curious why you might think it useful. Base64 is a transfer encoding sort of a thing, and concatenating blobs while in their transfer encoding just… isn’t something that you do.
It’s experiencing breaking changes in the name of improving performance and flexibility. This is precisely the reason why you don’t want it in the standard library, because that would have either prevented these improvements from being invented, or fractured the ecosystem between std::base64 and base64 in just your urllib/urllib2/urllib3/requests sort of way.
As it stands, by being external, people can depend on base64 0.11, 0.12, 0.13, &c. and it all works fine, and over time they’ll tend to migrate to the latest version (dealing with the generally minor breaking changes involved) but it doesn’t matter so much.
The ecosystem is already fractured! Depending on when you added the base64 crate to your project, you have incompatible interfaces! This seems bananas. This isn't, like, serde. It's base64!
Perhaps I didn’t express it in the clearest way; what I meant was more irredeemably fractured with no intent or expectation of reconciliation. When it’s split between base64 0.11, 0.12 and 0.13, it’s reasonable to expect people to update to the latest version of base64 over time—and hopefully that will eventually just be "1", but if it takes time to get there that’s fine.
The incompatible interfaces thing can be a problem in Rust and does require a bit of care, but in this particular case isn’t a serious problem; the base64 crate is almost always going to be implementation detail, not exposed publicly. With serde, yes, reaching a stable 1.0 was definitely more important for ecosystem compatibility.
If I want to dunk on Rust going forward, I can just say "it is important to know the difference between 0.11, 0.12, and 0.13 of the base64 crate". I think this is a hard argument to rebut. My point is not that Rust sucks, but that the stdlib strategy they've pursued is wrong; not a strength of the language.
Further: this seems like an easy problem to fix, much easier than "Go lacks match statements and generics" or "Python lacks meaningful typing altogether". But my guess is that culturally, it's just as hard to fix.
It’s not important. The differences are tiny, the sort of thing that would probably pass unnoticed in dynamically-typed languages but which Rust’s greater rigour instructs be marked a breaking change. As for knowing, humans aren’t in the habit of memorising every detail of libraries that they only interact with occasionally, and this is such a case.
Besides that, you’ll normally be dealing with the latest version, and if you deal with an older version then you should normally consider upgrading it. I looked through a few projects of mine with base64 deep in their dependency trees, and it was all 0.13 except for one instance of 0.12 via a dependency that needs updating.
You’re stubbornly ignoring the problem of freezing base64 into the standard library, apparently obdurately refusing to even acknowledge that a thick std is every bit as problematic as a thin std, just with a different set of caveats.
If you freeze base64 into the standard library, it’s frozen. No more even slightly incompatible changes can occur, and so you’re stuck with something inferior for ever after. This is exactly the urllib/urllib2/urllib3/requests situation. This is the “standard library is where things go to die” problem.
Rust’s chosen standard library strategy is in no way wrong; it’s just a different set of trade-offs, and incidentally one that experience says is decidedly preferable for a language of Rust’s complexity (wherein there is routinely not one simple and obvious way of doing a thing, in contrast to languages like Go, Python and JavaScript which have distinctly lower hazards to freezing things into the standard library, yet even their standard libraries have wilted in places).
I think it's you who are ignoring the problem. You say that by freezing a particular Base64 interface into std, Rust programmers lose the opportunity to take advantage of more efficient interfaces. But that's obviously not the case. std can just host the best general-purpose Base64 interface, a "good enough" implementation, and people who are fussy about their Base64's can use any crate they like. That's what happens with the Go standard library, and, in fact, it's also what happens in Rust (for instance, with the Nix crates).
I don't think you've thought this all the way through. This last response of yours seems more reflexive than contemplative; for instance, it suggests that breaking changes are no big deal, because they're small breaking changes. That's not how breaking changes work! Indeed, I feel like everyone who's ever worked in a totally undisciplined package ecosystem (I'm not saying Rust is one of those) knows what the end result of this is: hours of brain surgery figuring out how to reconcile incompatible dependencies-of-dependencies.
> You say that by freezing a particular Base64 interface into std, Rust programmers lose the opportunity to take advantage of more efficient interfaces. But that's obviously not the case. std can just host the best general-purpose Base64 interface, a "good enough" implementation, and people who are fussy about their Base64's can use any crate they like. That's what happens with the Go standard library, and, in fact, it's also what happens in Rust (for instance, with the Nix crates).
nix is not a good example. Nix gives _additional_ APIs that std doesn't provide that are specific to Unix.
This isn't about std-having-base64 preventing the use of external third party implementations. This is an absurd interpretation of what chrismorgan is saying. They're saying that std's implementation is frozen forever. We can't change it. One of three things occurs:
1. We got the API right and everyone is happy in every use case.
2. We got the API really wrong and now we probably need to deprecate it.
3. The API is too simple to permit maximal performance, and now people need to know a decision procedure to choose between std and a third party library.
You seem to think (2)+(3) is okay, but I'm actually not a huge fan of either. (2) causes churn and ecosystem pain. (3) causes confusion and becomes the butt of a joke: "yeah you actually shouldn't use the base64 stuff in std since it's so slow, so just use FooBar's implementation from crates.io instead." It's not unlike the JSON situation in the Go world: I am routinely having to replace the standard library's JSON implementation with a third party one that's faster. The difference is that "good enough" in Go-land is much more culturally acceptable than it is in Rust-land. In Rust land, as I said in an adjacent thread, everything gets sacrificed at the alter of performance. When people come to Rust, they generally expect to get the fastest of everything. Otherwise, it's not good enough to supplant C or C++.
The result of this is that adding things to std is a risk. So when we add things, we need to do a risk analysis and evaluate its benefits against its costs. When you nitpick a specific example like base64, it's easy to lose sight of the bigger picture here. Bringing base64 into std wouldn't solve the problem you're complaining about. (If it did, I have no doubt we'd do it, because it's base64. Probably we can get that right.) What you're really advocating is a philosophical/cultural change that will dramatically increase the size of std, and thus increase the risk we take with respect to introducing things that people will ultimately wind up not using. We don't want to do that. We want the ecosystem to work that out so that they have room to evolve.
> knows what the end result of this is: hours of brain surgery figuring out how to reconcile incompatible dependencies-of-dependencies
This generally only occurs for public dependencies. base64 is very unlikely to be a public dependency. Folks who maintain foundational public dependencies are generally quite careful about pushing our breaking change releases. (`rand` is a good counter example, but it looks like they've slowed things down as of lately.)
I'm not a huge fan of taking forever to get to a stable 1.0 for core ecosystem libraries, so that's definitely a knock against our approach. I'd rather see folks commit to a stable interface for some years even if the API isn't perfect in order to minimize churn.
I can assure you that my position is not some reflexive HN drivel. I've been on the library team since the beginning. I've thought a lot about this and lived through the trade offs. And chrismorgan is right: this is a decision with trade offs. There is no obviously correct choice here. I probably hate bringing in dependencies as much or more than you do.
Just a note that I did read this, wanted to reply, but felt I needed to write a reply that did it justice, and then it fell off my radar. We will meet again on the fields of language battle, burntsushi! (Thanks for taking the time to write this, even though I disagree with a bunch of it.)
You could use Rust editions to allow evolving the standard library, perhaps - they changed the prelude (slightly) in Rust 2021. That might come with its own problems - on the other hand, you can't actually tell the major version of any given library from code (unlike with Go), so changing the definition of a standard library package upon an edition change.
Are you referring to base64 here? If so, then you are very very wrong. I've written lots of Rust programs over the years and have needed base64 precisely once IIRC.
Take expected (which missed C++ 20 but may land in C++ 23). The idea here is like Rust's Result type†, but C++ lacks a sum type so it's instead presented a bit like a smart pointer. You check it for errors and then just dereference expected to get your answer if there weren't any errors. Simple. If you can't be bothered to do error-checking, no worries, dereferencing expected when it's an error throws an exception, and your usual C++ exception handling applies.
But no, the committee says this doesn't look dangerous enough to us, so they required it to be rewritten to say dereferencing expected when it's an error is Undefined Behaviour instead of throwing an exception.
Imagine deliberately adding more Undefined Behaviour to a programming language (technically in this case the language's standard library) in response to work to compete with safer languages. It's an act of self-sabotage without question.
† Unlike exceptions, in Result your error is just more data, to examine, process, store and use now or later or not at all at your option. An exception changes control flow in the program when the error occurs, which may not be what you need at all. This is crucial to some designs.
Yeah, the "there should not exist any language underneath besides Assembly" motto pushed to extreme, is what might eventually sink all efforts to make C++ safer alternative than plain old C.
I don't see this part though. "Leave no room for a lower level language" doesn't require you to go around defining every mistake as Undefined Behaviour, that's crazy. A different part of the committee managed to see in 2021 that if your format can be statically detected as bogus (e.g. you tell the formatter you're giving it a string, then hand over a floating point type variable) then the compiler should be obliged to emit a diagnostic and give up compiling the program, not hand you a program that it knows can't work and enjoy your humiliation when it fails in production. That's the sort of safety improvement you'd want to see.
As I understand it the runtime performance-at-all-costs school, which includes people from Google, don't want the C++ language to be deliberately unsafe they just don't prioritize safety over performance. This group won't allow C++ to have runtime bounds checking for the array subscript operator or its overloaded equivalents and I think they're wrong about that, but it's not making the problem worse. Stuff like ranges, views and iterators (even though C++ iterators are incredibly ugly) mean in idiomatic C++ today you're rarely reaching for this particular gun that's supplied loaded and pointing directly at your feet. In contrast what was done to expected just seems like self-sabotage. If it's too slow then don't use it. Or rather, as these same people would be the first to point out, measure to check your intuition that it's too slow and only then if it really is don't use it.
That is exactly what I mean, the leave no language underneath agenda is being pushed by the performance at all costs crowd.
In all these years I have left RTTI enabled, used exceptions, tried to keep bounds checking enabled in STL types, and it was never an issue in production.
std::array will instantiate function templates for all used array sizes, lengthen compilation time, and be hard to use across separately-compiled code without putting code into headers. It has its uses, but more often I'd prefer T[N] with bounds-checking, and, in another world, a builtin variably-sized, non-resizable array type; basically a sort of absl::FixedArray[1], but with the semantics of operator[] and at() reversed, so that bounds-checking is the default. Sadly, nothing like that is included in the standard, so won't be a lingua-franca.
That is the thing, the standard doesn't require bounds checking for operator()[], while it does require for at(), but it doesn't forbid it either, it is up to the implementations.
Rust and JS both have a very small standard library. This is what unites them in a 'dependency hell' of having to handle thousands of packages for most projects.
Other languages that don't face this, e.g. Python, have very large standard libraries which already include a dizzying array of features. E.g. take a look at some of the things you get with standard Python[0].
- sqlite3 (DB-API 2.0 interface for SQLite databases)
- bz2 (Support for bzip2 compression)
- sunau (Read and write Sun AU files)
- netrc (netrc file processing)
- curses (terminal handling for character-cell displays)
- mailbox (manipulate mailboxes in various formats)
The obvious compromise would be to let libraries bake as third-party projects for a good long while until there's a fair amount of consensus that they're the Right Thing To Do, then pull them into the standard library. Java's Joda-Time would be one example.
Oracle doesn't own Rust though, there's no real push to onboard these features besides the general curb-cutting efforts for newcomers. For the most part, 'integral' crates like serde and tokio have been community-managed without issue so far, so it doesn't make much sense to pull one open-source developers passion project from them and pass it to another.
I don’t think this is a characteristic of languages or a large ecosystems. It’s a side effect of dependency managers making it seem like zero cost to include another library. Including one dependency can end up pulling in an entire ecosystem of connected dependencies, when the perhaps one function in the original dependency.
One recent build I did, a small library included a dependency to build its documentation. This was a full-fledged documentation project, which pulled in hundreds more dependencies.
It would be interesting to see a display of “cost” in projects indicating how much additional dead weight each individual dependency pulls in. This might lead to slight better decisions on dependencies to choose.
> It would be interesting to see a display of “cost” in projects
cargo-bloat is kind of along these lines, although it only measures the absolute size of each dependency (as opposed to "dead weight") and has some platform limitations.
" it is a necessary side-effect of large ecosystems"
It depends a lot.
Java has a huge ecosystem, but almost all of the core functionality is provided for you.
Like everything there's a 90/10 rule in that 90% of the 'things we need' really amount to only 10% of the total packages out there, so it actually makes sense that the people backing the platform provide for it.
Language, compiler, debugger, documentation, tutorials, standard libraries etc. - it's all part of the platform.
Looking at it that way, it becomes a little more obvious why even some cool languages don't quite breakthrough.
As another commenter pointed out, it may relate more to the size of the standard library than to the size of the ecosystem. Which your comment seems to emphasize as well.
True in js/ts world this is a problem. Since couple of years I've been experimenting with more radical fat trimming on dependencies. It works very well on backend code. I belive shallow or no dependencies is the way to go.
What seems to be missing is standard library as constellation of well designed modules/packages. I decided to just do it for myself (and anybody who finds it interesting). I don't think it's going to fly high but if somebody is interested the link is here [0]. I'm adding modules every few days.
The aim is to have something worthwile to show around the time node v16 goes lts. I have some experience taking care of high stake business critical systems. This is fun side project as a hobby atm. Modules being dropped there are reflection of problems I'm facing during my day-to-day work.
You'll see there some non conventional code. Large parts are inspired by functional programming, ocalm and simplicitly in general. Personally I find it exciting how well this kind of approach fits into production projects.
The nice thing about this approach is that you can have niche leaf packages/modules as part of standard library (constellation) - if you don't want to use it, it doesn't matter, it wont incur any cost. For example order book module or exchange matching engine - those can simply live there, be useful to people who care and have zero impact to people who don't care at all. I find it to be an interesting difference from builtin, shipped library.
Large package ecosystem is a sign of popularity. With time libraries should converge onto shared approaches. Interestingly practice shows wide spread of duplication. I think more effort from big players with wide reach could reduce it. Ie. not me doing stuff but Microsoft or others picking up the ball.
Yeah Go just has those as internal dependencies in the stdlib.
That _is_ the tradeoff - Rust builds up from nothing and makes it friction-less to add a new dependency. Go starts at a higher level and the friction for adding new stuff is high. And some of the high-level stuff, like the GC, can't be removed.
Rusts ability to squeeze into small footprints is a huge part of the appeal. I don’t think that that excuses the exceptional difficulty of writing a web server or client in rust relative to other languages.
How often are folks writing applications that don’t need to connect to the web these days?
I just tested this. It took me 43 second to build a new project with reqwest as a dependency. The second build took 0.55 seconds. If that's exceptional difficulty, I'm underpaid.
Maybe the lesson here is to update Rust if you haven't used it in a while?
I didn't find writing a simple web server or client in Rust any harder or simpler than in e.g. Python. What is this exceptional difficulty we are talking about?
> How often are folks writing applications that don’t need to connect to the web these days?
In Go? Rarely, I would expect. In Rust, I'd assume connecting to the web is the exception rather than the norm. Most Rust projects tend to be lower-level infrastructure & systems programming.
This probably explains, and in turn is explained by, their different choices re: http in stdlib. They have much different aims and target use-case.
`ureq::get("url").call().unwrap().to_string().unwrap()` with a stopwatch should get you where you want. Add ureq as a dependency, of course, but I found ureq to be better than reqwest with about half the dependencies.
For example, one shock is that you can't print! or dbg! anything (without adding Debug/Display to each type).
The reasons?
Where it will print in a Airpod? yeah: Rust is made to work in places where NOT exist a place to print to. Or where NOT exist a filesystem, or HTTP because not exist TCP because not exist a network card.
So, in Rust all start in the deepest of the deepest bottom.
---
I have a good metric to select frameworks/libraries:
- If is too complex, NO except if is the only game on town
- If have crazy deps setup (ejem: OpenSSL, bastard mess of dep!) NOOOOO. (except if is the only game on town)
- If is too big, damm be good and solid (ej: PostgreSQL)
Whatever the lang, this have served me well, and when I have been lazy (I get a deps to OpenSSL and waste 1 week fixing CI builds with it then remember this and cut that mess) the pain quickly fix it.
So, next time you get a trouble like this (in Rust or whatever) ask what alternatives you can use instead!
Rust went for "evergreen" approach like Chrome. It focuses on making small gradual changes, quick and painless upgrades, and provides stability via backwards compatibility in the compiler. This allows everyone to only target one version: the current one.
Debian's philosophy is the polar opposite, so Debian's Rust version is doomed to be perpetually obsolete.
All past Rust releases are available. They can be easily installed via rustup, and multiple versions can coexist on the same machine (even run concurrently). That's even easier and more flexible than using old Debian packages.
In addition to what others have said, reqwest is notorious for being one of the more heavy-weight HTTP libraries, but luckily, it's far from the only option: https://www.arewewebyet.org/topics/http-clients/
I think several of the responses to your comment are misguided. If you believe them you'd think that the reason this is to be expected for Rust (and even normal! good!) is that (a) Rust is a systems language, (b) Rust deliberately has a small standard library, and (c) it doesn't matter anyway because compiles are cached.
I'd say, first of all, that it does matter, because very few projects with hundreds of (transitive) dependencies are going to be able to take security sufficiently seriously. And because minimizing the number of other projects you rely on is good engineering (see: left-pad).
I don't think the other two explanations are much better. Compare C as an example. With C you can use libcurl which is well-maintained, actually supports far more protocols than simply http(s), and uses far fewer dependencies to do it. It also runs on an insane number of platforms, so portability isn't the issue here either.
I think it primarily comes down to cultural differences. Rust is a newer, "cooler" platform, and attracts a very different audience than C does. Because of that Rust's users will be more apt to follow common modern programming practices: using a language's built in package manager and so on. (This culture both contributes to the development of languages like Rust and is fed by them.)
Remember the XKCD "Python" comic? [1] Python is what I would consider an older language in terms of culture, but it captures the idea of being able to just import a library that can already magically do everything you want. "Programming is fun again", you don't have to write boilerplate or worry about managing dependencies any more. The problem begins when everyone has that attitude, including the people writing libraries that will be used by others. Importing a library that can instantly solve your problem is just one line in your Cargo.toml away - who cares that it's got 200 dependencies!
Python's fine until you want to use a library that's not part of the standard library, then it's a nightmare. Especially if you want to distribute your code, not just run it locally. (Ditto for C and C++.)
I was prototyping something that could monitor a few api endpoints in parallel, collect timing stats using sqlite, then output a static status page, statistics and graphs.
Kind of like what smokeping does, just wanted to see what a from-scratch implementation that did just what I needed might look like.
Ended up being about 400 lines. Only dependencies were go-sqlite and go-chart.
The irony of cargo not supporting binary libraries is that the usual solution ends up reducing the number of dependencies, and being forced to rewrite ourselves the solution in another way.
If you want to set up something like this for your own use, look into sccache. Integrating this more broadly into cargo would be generally pretty useless without a stable Rust ABI.
Yeah that is a possible solution, but still not quite what vcpkg, conan and distribution repositories offer, or having COM/Windows Runtime libraries.
I look forward when cargo offers that as an option, doesn't mean everyone has to offer their libraries that way, just having the possibility on crates.io would be nice.
I am also very thankful for what has been achieved thus far, so don't take this as thankless complaint rather as I would like Rust one day to be.
Most building systems offer a way to cache build results (ex. gitlab), so the largest speedups usually come from there. Another way is to use an explicit “builder image” with packs your dependencies and an older version of the program into the starting point for the run.
Yet another way is to just use bigger machines if you’re CPU bound. Github and Gitlab generally offer 1/2 core 3-8GB RAM instances, but SurplusCI[0] (a project I recently launched) has no such restriction. Up to 8 vcore 32GB RAM machines available.
I'm not an expert on the language, but I see Rust's build times as fairly justified. Rust's focus is on execution speed and correctness, which means that the compiler needs to be extremely pedantic with CPU targets to know how to optimize your release. Once you have a target outlined, it has to acquire and build the latest versions of your dependencies with the highest level of optimization possible for your specific architecture... that's just a lot of computing, period. Caching is a half-solution to this as the author suggests, but frankly there isn't much juice left to squeeze out of the Rust fruit.
Now, there are still ways to make the compiler choke: the mention of instantiations towards the end is a great example. But in my (non-professional) experience, the build times are pretty tolerable for what it's providing.
The critics are right that the build times are slow even in debug mode, and that it's one of the most frustrating things about the language even for experienced users.
There are some alternate codegen backends that aim to fix this by compiling less-optimized debug builds more quickly.
Something I noticed since I've started timing builds is that codegen and linking usually takes less than 30% of the build time for an incremental build.
So in practice, switching to Cranelift and lld/mold will shave maybe 2 seconds off a 10-seconds build. Better, but still flow breaking.
If we want Go-like compile times, we're going to need some more fundamental improvements to the compiler.
> the build times are pretty tolerable for what it's providing.
I think this is probably right, and I think just about everyone "tolerates" the build times (that is, I see very few people stop developing in rust because of the build times).
But when, for example, I run `rustup update` to get a new nightly compiler and have to re-compile everything, it's annoying to have to wait 20 minutes. I'll tolerate it, and if a magic wand could make it instantaneous I would love that. But it's still annoying.
I have a low spec machine but i still patiently wait for 200 crates to download.Because it is cached. But the most annoying thing is that when I am using any frameworks like gui and game. The second compilation also takes a minute to start the app. I have ended up with them. :(
I didn't downvote it, but it feels like the parent comment is making excuses for rust build times being slow. As someone who's worked with C++, slow build times are a leech to productivity, and I expect new languages to have a better solution than "whatever."
If you want to cut the CI run time even further.
I'd recommend BuildJet for Github Action.
We give you managed high-performance CI runners, which cuts both your CI time and price in half.
We plug right into Github Actions, so it's just one line change in your yaml file.
Nobody seems to care, but there's a crate that does this for you https://github.com/llogiq/momo
But the Rust compiler should be doing this automatically :(