Most of this stuff isn't addressing what makes Rust harder than, say, Go or C#. The pattern matching makes it if anything easier, since pattern matched dispatch is way easier to read than a bunch of complicated if/else blocks.
Rust is not garbage collected. It's a systems language, so it doesn't ship or run inside a big runtime. Rust's RAII and lifetime system does an admirable job of making most of the headaches and unsafe aspects of manual memory management fade into the background... until you throw in async.
After having used Rust for almost two years, I can confidently say that async and its interactions with memory are the hard part. (Edit: Making async code properly clean up after itself is also the hard part, unless you use a better async runtime than tokio.)
There are two paths with async. One is to use Arc<> everywhere, at which point Rust degrades into a more verbose version of Go but without automatic handling of circular references like you get with a good GC. You still get a thinner runtime and deterministic execution, which are benefits of a non-GC language at runtime, but you lose a lot of the efficiency and ergonomic benefits. You also end up with circular Arc<> references very easily if you have a task that owns two Arc<> classes that own tasks that own each other etc.
The second path with async is to use a scoped / structured concurrency library that lets tasks have lifetimes associated with them. Unfortunately those all come with not-entirely-safe warnings when used with tokio, and the underspecification of async in the standard library means the ecosystem has fixated on the runtime with nonexistent structured concurrency support (tokio) as the standard against which all other async runtimes link. You could use a superior async runtime like smol, but then you'd have to vendor/fork all your libraries.
TL;DR: async is still half-baked. The rest of Rust is actually easier than C++ and can be almost as easy as a language like C# once you grok lifetimes.
Edit:
IMHO tokio made a massive mistake in avoiding lifetimes associated with tasks and structured concurrency, probably on the speculation that this would be more confusing. It's not. It's more confusing not to have this, since you basically lose the entire lifetime system and its benefits in multithreaded async code. You also lose RAII in async code.
Rust also made a massive mistake in not more fully specifying async within the standard library and structuring it to be either runtime-included or runtime-agnostic. Until this happens async is half-baked and broken.
> [...], I can confidently say that async and its interactions with memory are the hard part. (Edit: Making async code properly clean up after itself is also the hard part, unless you use a better async runtime than tokio.)
I would love to read more about this, are there any specific areas or things I might search for?
It annoys me to no end that the community has cargo-culted around tokio. Async should also have probably defaulted to single-threaded. Does work-stealing offer that much of a performance advantage over simpler thread per core?
100% agree it’s a huge mistake to water down Rust’s `enum` and `match` when teaching Rust.
If anything, these concepts (algebraic data types and pattern matching) are by far the highest “ROI” features to learn of all: they’re super easy to learn, intuitive to read and write, immediately useful, and unlock a level of rich, expressive, inherently robust code not possible in many other languages.
However, I do agree with the author that the borrow checker is the first area of “friction” for most, and so suggesting more liberal use of cloning is a completely fair strategy to make the learning curve less steep for beginners.
Rust is not garbage collected. It's a systems language, so it doesn't ship or run inside a big runtime. Rust's RAII and lifetime system does an admirable job of making most of the headaches and unsafe aspects of manual memory management fade into the background... until you throw in async.
After having used Rust for almost two years, I can confidently say that async and its interactions with memory are the hard part. (Edit: Making async code properly clean up after itself is also the hard part, unless you use a better async runtime than tokio.)
There are two paths with async. One is to use Arc<> everywhere, at which point Rust degrades into a more verbose version of Go but without automatic handling of circular references like you get with a good GC. You still get a thinner runtime and deterministic execution, which are benefits of a non-GC language at runtime, but you lose a lot of the efficiency and ergonomic benefits. You also end up with circular Arc<> references very easily if you have a task that owns two Arc<> classes that own tasks that own each other etc.
The second path with async is to use a scoped / structured concurrency library that lets tasks have lifetimes associated with them. Unfortunately those all come with not-entirely-safe warnings when used with tokio, and the underspecification of async in the standard library means the ecosystem has fixated on the runtime with nonexistent structured concurrency support (tokio) as the standard against which all other async runtimes link. You could use a superior async runtime like smol, but then you'd have to vendor/fork all your libraries.
TL;DR: async is still half-baked. The rest of Rust is actually easier than C++ and can be almost as easy as a language like C# once you grok lifetimes.
Edit:
IMHO tokio made a massive mistake in avoiding lifetimes associated with tasks and structured concurrency, probably on the speculation that this would be more confusing. It's not. It's more confusing not to have this, since you basically lose the entire lifetime system and its benefits in multithreaded async code. You also lose RAII in async code.
Rust also made a massive mistake in not more fully specifying async within the standard library and structuring it to be either runtime-included or runtime-agnostic. Until this happens async is half-baked and broken.