the core of rust
58 points by calvin
58 points by calvin
Within Rust, too, there is a much smaller and cleaner language struggling to get out: one with a clear vision, goals, focus.
On the meta level, I sort feel that Rust does have a rather clear vision, and that is being industrial language, which means accepting unfavorable tradeoffs in the interest of letting the users ship real-world software. Rust is an increasingly practical language. This is a different vision from what one might originally expected it to be — a safe low-level language. I wonder how Rust would’ve looked in an alternative universe, where C# wasn’t for the most of its life a window-only language, but rather started with .NET core and with features like spans.
On the language design level, “is there a smaller Rust, that’s still Rust?” is an interesting question to ponder! I feel like, in general, no. You need monomorphisation, traits, unboxed closures, and lifetimes to make zero-cost memory safe abstraction work. This is the core of Rust:
https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.map
That being said, it does feel like there’s a bit of gratuitous complexity, which could be removed without much harm. Name resolution can be massively simplified, I don’t think there’s a fundamental need to have 3 ½ namespaces, distinguish between .
and ::
, and allow for non-trivial mapping between modules and files. While there’s a clear need for some sort of the macros to make derive work, I feel this can be done simpler. Macros that introduce arbitrary custom syntax feel like they are not that necessary. Maybe the syntax can be simplified somewhat (<>
, .0
), but that’s minor.
And that’s … all I think that’s clearly simplify-able, language-wise? It’s not clear to me how we could’ve made async simpler (one option would be to bless stackfull coroutines as a library, and let sans-io people write state machines manually, but that’s not a clear improvement). const-fn I think could be simpler, but that’s not yet in the language even, so doesn’t count :P
I feel like, in general, no. You need monomorphisation, traits, unboxed closures, and lifetimes to make zero-cost memory safe abstraction work. This is the core of Rust:
This is the interesting part, I think: is that the core of Rust? We had the “Rust is more than safety” debate ten years ago! Different people have different opinions on what the core actually is.
For example, boat’s posts on this says:
Rust works because it enables users to write in an imperative programming style, which is the mainstream style of programming that most users are familiar with, while avoiding to an impressive degree the kinds of bugs that imperative programming is notorious for.
This is very different than yours! Boats would basically eschew the zero cost aspect to focus on the other aspects.
I myself am not 100% sure what the answer to this question is. I do have some thoughts though, I am giving a talk about this next week at Rust Forge.
That being said, it does feel like there’s a bit of gratuitous complexity, which could be removed without much harm
This I do agree with, there’s a bunch of stuff that was kinda “well this is what was implemented” around 1.0, and a lot of it is the sorts of things you mention.
Yeah, for me, Rust is mostly safety&speed (https://graydon2.dreamwidth.org/247406.html)
Obligatory link: https://brson.github.io/fireflowers/
On the meta level, I sort feel that Rust does have a rather clear vision, and that is being industrial language, which means accepting unfavorable tradeoffs in the interest of letting the users ship real-world software. Rust is an increasingly practical language. This is a different vision from what one might originally expected it to be — a safe low-level language. I wonder how Rust would’ve looked in an alternative universe, where C# wasn’t for the most of its life a window-only language, but rather started with .NET core and with features like spans.
this is very valid yeah, that it does have a vision and i just disagree with it. i think that’s true. i still think that “early rust” coheres much better with itself than many of the newer features though. maybe this is just an indication of which things are supported and reinforced by the standard library rather than anything about the language itself; e.g. slice.chunks() is never going to have a const generics API because that would be a breaking change.
i agree that Iterator::map is the core of rust. i think you could design a smaller rust that has Iterator::map without being a systems language (i.e. you get references but no raw pointers, you get Vec but no guarantees about stack/heap allocation). but again, that is a different vision, not necessarily a better vision.
While there’s a clear need for some sort of the macros to make derive work, I feel this can be done simpler. Macros that introduce arbitrary custom syntax feel like they are not that necessary.
there are some clear wins (e.g. support pub
on macro_rules seems unambiguously better to me), but in general I think macros would need to be redesigned from scratch to have a better design. i would love to see someone do for macro_rules what Amanieu did for llvm_asm.
e.g. slice.chunks() is never going to have a const generics API because that would be a breaking change.
slice.chunks()
can’t have a const generics API not because it’s a breaking change but because the final chunk may not be the same size. But slice.chunks_exact()
can, and the const generics version of that is slice.array_chunks()
. More generally, there’s uses for both compile-time– and runtime-specified chunk sizes, so having both versions in the language is good.
That last link is probably meant to point at https://doc.rust-lang.org/stable/std/primitive.slice.html#method.array_chunks. Also, there exists a stable variant of this now with slice.as_chunks()
.
Oops, I didn’t notice I got the wrong link there, thanks. as_chunks()
also does seem a bit more general, s.as_chunks().0.iter()
would even give you the equivalent to array_chunks()
. Though for slice.windows()
we still need slice.array_windows()
, there’s no way to write slice.as_windows()
.
i still think that “early rust” coheres much better
Yeah! I was actually independently mulling over similar topics today, and the way I put it inside my head is that Rust 2015 was a more complete language than Rust 2025 is.
That’s some nostalgia. Support for arrays and static
initialization was a mess. Const evaluation didn’t exist apart from a couple of stopgap hacks. Traits couldn’t work on arrays of arbitrary size, so arrays with lengths > 32 were painful to use.
There was a ton of post-1.0 language fixes for things that were missing (overloading of +=), or buggy (packed structs, fn types, macro_rules), or undefined (bool ABI). Panics couldn’t be caught, unwinding couldn’t be disabled.
Namespacing may have been theoretically more coherent, but every new user was baffled why std::
works in use
everywhere, but in expressions only sometimes.
Pattern matching was technically simpler, but also painfully tedious whack-a-mole of & and ref.
No SIMD. No ? operator. No good way to work with unit data.
Borrow checker was sensitive to order variables were defined, and often required extra blocks nested inside blocks to avoid clashing with the bizarre drop order of returns.
It was rough.
I’m not even counting all the basic stuff that was missing from std, and that generics were severely limited and there were lots of things you just couldn’t do.
You are misunderstanding the point. A language which doesn’t do any compile-time evaluation is a more complete language than the one that supports only half of itself at compile time.
Rust 1.0 had compile-time evaluation for array lengths and static initializers. You could have defined const FOUR: usize = 2 + 2
and [u8; FOUR]
, but not much besides that, because the const interpreter only understood a handful of AST nodes.
Supporting 5% of the language at compile time wasn’t more complete to me than supporting 50%.
1.0 was not a done language, only an MVP that shipped what worked at the time. It had lots of little arbitrary gaps.
It’s not clear to me how we could’ve made async simpler
Oh, actually, I forgot this argument https://matklad.github.io/2023/12/10/nsfw.html which I think holds water. TL;DR is that we track Send
ness of futures somewhat by accident, and that, if we had only made thread locals as safe as global variables (that is, unsafe), we wouldn’t have to solve the problem of conditional Send
bounds, and that feels to me like a meaningful simplification.
That post is interesting, I hadn’t seen it before, but at the end it proposes to make thread-locals unsafe as a way to make Send abstract over execution context, and that’s glossing over the problem of OS APIs that require same-thread behavior, e.g. pthreads and MutexGuard
. Defining TLS as unsafe doesn’t solve the problem where the hypothetical better async spawn would allow you to write
tokio::spawn(|| {
let mutex = Mutex::new(0);
async move {
let guard = mutex.lock().unwrap();
tokio::task::yield_now().await; // task reschedules here
drop(guard); // dropped on wrong thread
}
});
This version does still require the OsThreadSend
trait, unless Mutex
is written to never use pthreads anywhere. I don’t know offhand if there are any other OS APIs in libstd that have this constraint too, or any in common third-party crates.
I think there are deeper simplifications that are possible in the way lifetimes are integrated into the type system, though I couldn’t be specific about what they are and I don’t know that they would change the user experience, just hopefully simplify the implementation.
In a world without mod foo {}
, would you manually mark all of your tests #[cfg(test)]
(or maybe have #[test]
implicitly do that)?
#[test]
already does that: https://github.com/rust-qualification/rust-reference/blob/master/src/attributes/testing.md#the-test-attribute
These functions are only compiled when in test mode.
cfg(test)
allows skipping the entire module, and avoids unused warnings for imports. But strictly speaking it’s not necessary.
Just a note on the reference to my old post: my “smaller Rust” was smaller because it gives up on “systems programming;” the point was that you could design a language that still applies lifetime analysis for managing state in a context where maximizing performance is not critical, and so give up a lot of the complexity of Rust. It wasn’t smaller because it was the purer expression of Rust’s essence, it’s just applying similar techniques to a different set of design goals, with the idea that this will produce a language that has fewer moving parts and is easier to pick up.
That JavaScript example requires more concepts than the author mentions. As a non-JS dev it’s not immediately clear what is going on with await (new Promise(() => {}));
for example. You could argue that “this can be guessed”, but you could argue the same thing about the Result
example in Rust earlier.
Not that this changes the main points of the article much, but it irked me a little.
Not disagreeing with any of your points, just a remark: the await new Promise(() => {});
isn’t actually needed for the JavaScript program to work, unlike the Rust program where one must explicitly prevent the program from exiting prematurely. The Node.js runtime (and other JavaScript runtimes) will arrange for the program to continue running as long as the file watchers are active. More generally, the event loop will continue running as long as there are active handles; see the libuv documentation.
It’s difficult to say whether this sublety makes the JavaScript program more or less complicated. On one hand, programs that start file watchers, web servers, and other long-running operations will just work and not exit early without the programmer having to think about it. This is perhaps a desirable property for a higher-level language. On the other hand, the mechanism that allows for this to happen under the hood is more difficult to explain–certainly more difficult than “just block indefinitely at the end to prevent exiting”.
i know rob pike gets a lot of flack for “[new grads] are not capable of understanding a brilliant language […] the language that we give them has to be easy for them to understand and easy to adopt”, but it’s hard not to see that point being made here
rust, albeit a brilliant language, is not one that’s easy to understand or adopt, in comparison to sloppier languages, where you can get a lot of stuff done with only knowing 10-20% of the language proper
I very much think that readability is the core of this: I learned golang by just reading code, and fixing some bugs in the open source software I was using. C++ was not the template to follow for something easy to pick up.
Expect little of people, get little of people.
From the inscrutable commentary above (“3 ½ namespaces, distinguish between . and ::, and allow for non-trivial mapping between modules and files”), I’m quite validated in this feeling.
The commentary is inscrutable, but I’m a moderate Rust user and… the two latter points are not as inscrutable as they sound; .
vs ::
still hurts my muscle memory from other languages a bit, and the module flexibility I also don’t get used to, but I understand why they are like this and IMHO they are not major issues.
(The first thing you quoted I have some impression that it’s about some stuff that my more advanced colleagues have to fight sometimes, it’s not really something that affects my use of Rust [because I do simpler things than them].)
I do struggle with the mental complexity of ownership, but mostly because I have not ever had to deal with ownership, but I think it’s the tradeoff you have to accept if you want to use Rust where it has an advantage over its competitors. I think that if you have significant experience with C/C++ or whatever, then because you already know how to deal with ownership, it’s a completely different game.
…
The problem is, Rust is IMHO so well designed, that you want to use it even where you could get away with simpler languages where you don’t really need to deal with ownership. If you want “it compiles it works” or you feel that programming is proving theorems, you don’t have so many options with a good ecosystem!
…
I’ll tell you, I still cannot deal with ownership in a rational way where I can just reason over it mechanically in my head, but I can still get a lot done without knowing what I’m doing. And when I hack on Rust for a while, my brain starts to write programs in the right shape that keeps ownership problems away. (For simple programs.)
. vs :: still hurts my muscle memory from other languages a bit
I’ll defend this a bit. I think multiple namespaces are generally underrated as a language design tool. In Rust I can have use std::iter
and a local variable named iter
and I can still refer to the module namespace unambiguously with iter::once
.
In Rust there’s also the additional dimension that .
probably has the most automagic semantics of anything in the language (auto-ref and auto-deref, and the latter can perform arbitrarily many levels of calls to deref/deref_mut for custom trait impls of Deref/DerefMut) which to me further justifies the existence of a notation like ::
which merely denotes path traversal.
In other words, if you’re going to have an automagic DWIM notation like Rust’s dot notation (I’m not arguing whether you should or not) then you really need to offer a completely explicit alternative notation. That’s orthogonal to whether you want multiple namespaces but in Rust’s case the design questions intersect.