Rust Is Hard, Or: The Misery of Mainstream Programming
I personally believe that Rust is an exceptional language and it’s ahead of its time. Seriously, can you think of any other language with as incredible type system, incredible tooling, incredible performance, incredible package management and general package quality, incredible compilation target support, and practically no few backwards-compatibility quirks?<p>But Rust is a really bad <i>general-purpose</i> language. Because Rust isn’t a general-purpose language. It’s designed for <i>high-performance, low-memory, safe</i> computing. The only reason it’s even remotely considered “general-purpose” is because of how great it is. Unless you actually need to write something that’s high-performance, low-memory, or safe (or there’s a really important package in Rust that you can’t find anywhere else), stick to something like Swift or Kotlin or Java or TypeScript. Or even JavaScript or Python or Lua if you’re only writing small scripts.<p>-<p>Rust abstracts almost <i>nothing</i> about the OS. In order to understand Rust, you need to understand at a low-level how computers actually work and how languages compile. You need to understand the what, why, and how of threads, the stack &#x2F; heap, I&#x2F;O, <i>endianness</i>, etc.; and language features dynamic dispatch, references, async runtime, memory management etc.. These language features are all implemented in Rust, but they’re explicit, you have to think about them and choose your implementation.<p>People say that Rust is hard because of the borrow checker but there’s much more than that: dyn Traits (you have to understand when a trait can and cannot be dyn), Unsized types (and why there are so many type parameters, because everything must be sized), closure traits, multi-threaded communication, what async actually does, etc. Heck, we almost got you to manually choose <i>allocation schemes</i> (they’re default type parameters, which is weird because Rust doesn’t have many implicit defaults).<p>You often face decisions like<p>- should this trait be a static &#x2F; type parameter, or dynamic &#x2F; Box&lt;dyn Trait&gt;?<p>- Is this value borrowed or owned or Cow?<p>- Should this be a unique reference &#x2F; Box or a shared reference &#x2F; Rc or a shared-across-threads Arc or not a reference at all?<p>- How do we control this value and communicate across threads, do we make the whole thing thread-safe or message passing or provide a shared Mutex&#x2F;AtomicBool?<p>- How do we manage async, do we set up an async runtime ourselves (and if so how do we configure this runtime), or how do we add integration with existing async runtimes?<p>- Should this be a Cell &#x2F; RefCell because we don’t want to or can’t pass a single mutable reference around? And when &#x2F; how do we dereference the RefCell as to ensure it doesn’t get accessed anywhere else until the dereference is dropped?<p>- Can we do this safely, aka encode it in a way the compiler understands? And if not, how do we a) do it correctly unsafe and b) make the unsafe code as short as possible?<p>-<p>And this explicitness is part of what makes Rust so amazing, because other languages make these decisions for you. But every decision has costs, and either they a) incur higher overhead, or b) lose safety guarantees.<p>e.g. in JavaScript everything is on one thread, so no data-races but no true parallelism; everything is garbage collected, so no use-after-free or double-free but you get GC spikes; everything is a dynamic boxed reference and types are checked at runtime, so no passing complex types and type parameters everywhere, but calls are much slower and there is no true type-safety.<p>Or in C++, there are multiple threads with shared memory but you must explicitly avoid data-races; there is no GC overhead but you must explicitly manage allocs&#x2F;frees; you can choose whether to pass&#x2F;store things as references or values and there’s a type system, but values can be assigned &#x2F; casted to the wrong types leading to memory corruption.<p>Rust is the only mainstream language where you can do unsafe-but-efficient stuff, but it’s actually safe because it’s checked at compile-time.<p>-<p>But this explicitness also makes Rust a bad general-purpose language. e.g.<p>If you want, you can pretty much make everything in Rust like this. #[tokio::main] for the implicit async runtime, everything Gc&lt;RefCell&lt;T&gt;&gt; for no borrowing concerns, dyn Any with upcasting &#x2F; down casting, etc. This is mainly at the cost of a <i>ton</i> of extra syntax. And of course you get higher overhead and lose compile-time guarantees, which are now checked at runtime.<p>Or better yet, what you should <i>actually</i> do, is write your code which doesn’t need high performance, low memory, or security guarantees in another language. And possibly interface it to Rust code with FFI.<p>Because Rust isn’t a general purpose language. It tries to be general-purpose, and is to an extent, but it’s ultimately built for niche performance and reliability guarantees, and you just can’t have those without user overhead.