The Disappointment of Rust: A Critical Examination of Its Practicality
This article critically examines Rust's claims, arguing that its slow compilation, inherent complexity, and compromised reliability in pursuit of memory safety limit its practical utility, especially for applications involving mutable shared state.
Initially, I adopted the persona of a "Rust hater," largely as a response to the pervasive fanboyism surrounding it, often highlighted by its consistent top rankings in Stack Overflow's most-loved language surveys. While C++ undoubtedly presents numerous challenges – many of which I strongly dislike – the widespread anticipation for a truly superior programming language often leads to a sense of disappointment upon encountering Rust.
Rust, despite its promises, presents several fundamental issues:
1. Slow Compilation
Rust's compilation is notoriously slow—significantly slower than C++. While it has seen performance improvements over the years, becoming several times faster, objectively, a two-order-of-magnitude increase in speed, not just a twofold one, is what's truly needed.
This isn't a temporary problem; it's by design. As one of its designers explains, compilation speed was often sacrificed for other priorities. Although efforts like a better frontend and MIR (Mid-level IR) have been undertaken since 2015 to optimize it, they have yet to significantly accelerate compilation, though they do speed up compiler checks.
Fundamentally, making Rust compile fast is challenging because the problem is inherent to generics-heavy languages like Haskell, to which Rust bears a strong resemblance. It's also akin to template-heavy C++, which suffers from the same slow compilation issues. For instance, a simple for i in 0..limit {} loop in Rust doesn't just iterate; it creates a range, an iterator, and then iterates over it, all of which are monomorphized to concrete types and must be individually optimized. Non-optimized Rust code is often too slow even for debugging. Add a non-optional borrow-checker into the mix, and you have an inherently slow compiler that necessitates frequent recompilations due to its relentless nature.
2. Inherent Complexity
Rust is just as complex as C++, yet C++'s complexity can often be attributed to its legacy, whereas Rust started fresh. The constant need to navigate structures like Arc<Mutex<Box<T>>> directly impacts the quality of implemented logic, making it difficult to grasp the bigger picture. Since C++ shares this problem, the ultimate benefit of switching languages becomes questionable.
This complexity is unavoidable. Developers cannot simply choose to write high-level code for a cold path without needing deep performance or lifetime handling. Rust forces engagement with low-level nuances at nearly every turn. Without a garbage collector (and there will never be one), data must be semi-manually organized into an ownership tree. Fluency in ownership, borrowing, and traits is essential for even a few lines of code.
As a result, writing high-level logic in Rust is remarkably difficult. This is why many early Rust adopters have reverted to languages like Node.js and Go for less performance-sensitive services; the combination of high complexity and slow compilation makes it impractical to develop anything intricate using Rust alone.
3. Memory Safety: A Costly Priority
Rust's core principles are uncompromising: performance and memory safety. However, I contend that Rust's designers went overboard with memory safety. It's noteworthy that even Rust's own containers are implemented using unsafe functions, acknowledging that perfect correctness isn't always achievable within the Turing machine model; safe programs are built from carefully arranged unsafe blocks. Languages like Node.js and Go are considered practically safe, yet Rust seems to have sacrificed sanity and practicality for an abstract ideal of memory safety, often without achieving 100% memory safety in the end.
Practically speaking, many use cases don't require absolute memory safety. Unsafe programs can be designed to avoid remote code execution or secret leaks, instead only corrupting user data or acting sporadically. If a pacemaker fails, telling the victim that "memory was not corrupted in the crash" offers little comfort. A recent Cloudflare outage, for instance, was caused by a crash on an unwrap() function. This incident strongly supports my argument: Rust prioritizes memory safety, but at the cost of reliability and developer sanity. This is why I believe the language designers went too far, sacrificing a core practical quality for an abstract principle, much like Haskell designers sacrificed practicality for purity.
4. Challenges with Mutable Shared State
While mutable shared state is possible in Rust, its implementation often negates most of Rust's advantages while retaining its deficiencies. Most successful Rust projects leverage shared read-only state, one-way data flow, and acyclic data structures, such as the rustc compiler, mdbook and pulldown-cmark Markdown tools, stateless handlers like Actix and Axum, append-only blockchains, and single-threaded WASM. This model closely parallels Haskell's strengths in parsers, stateless handlers, mostly non-interactive CLI tools, and blockchain applications.
Early Rust prototypes considered Software Transactional Memory (STM) for safe concurrency, but STM incurs performance penalties and requires significant runtime support.
Stepping into shared mutable state often means memory corruption is the rule, not the exception. Handling these corruptions becomes necessary, as simple crashes are insufficient. Borrow checker and ownership mechanisms become less effective; analyzing ownership in cyclic graphs typically requires GC-like algorithms. Sync/Send, Mutex, and reference counting (Arc) are safe but inefficient for intensive multithreaded communication due to their impact on CPU caches, undermining Rust's primary promise of performance. Thus, engaging with shared mutable state often means losing all of Rust's perceived advantages, which makes sense given that its main concept was to avoid shared mutable state altogether.
GUI development, fundamentally, involves mutable shared state. This explains the scarcity of significant GUI projects in Rust. The Zed IDE, for example, has been in beta for years, illustrating the difficulties developers face navigating the borrow checker to implement core logic and features. Similarly, large databases, scalable stateful services, and operating systems (or significant Linux modules) are yet to see widespread Rust adoption.
C++: A Foundation of Flaws
C++ itself is undeniably problematic. Undefined behavior (UB) is a fundamental aspect of the language; one doesn't merely encounter UB—the entire language is built upon it. Array indexing, for instance, immediately leads to UB because the language does not inherently check out-of-bounds access. It's crucial to emphasize that many UBs are not even justified by performance considerations; they represent an outright sloppy design carried over and amplified from C. The list of C++'s flaws is extensive:
- Implicit type conversions, copies, constructors, and object slicing—essentially everything implicit.
- Function overloading (implicit), especially its omnipresence in the Standard Template Library (STL).
- Non-uniform error handling, with exceptions feeling like an afterthought.
- Still relying on
#include-ing text files 40 years after C, with the One Definition Rule barely checked by compilers. - An unsound combination of paradigms, making tasks like overriding generic functions in descendant classes highly problematic.
- SFINAE (Substitution Failure Is Not An Error) as a core, yet often cumbersome, mechanism of generic programming.
- A proliferation of types like
T,T&,T*,std::optional, andstd::unique_ptrto describe similar concepts, each with its own quirks, further complicated byconst.
Given C++'s inherent complexity, unsafety, and slow compiler, how effectively does Rust truly address these fundamental problems?
Summary
Is Rust a good or bad language? It's neither. It is a mediocre programming language. However, the thousands of man-months invested in its development make it a viable tool, readily available for use. For instance, this blog was generated using Zola, a static site generator written in Rust, without requiring me to write a single line of Rust code. Rust is well-suited for Zola due to its non-interactive nature and one-way flow of immutable data. However, it's essential to temper expectations and avoid proclaiming Rust as the universal solution for all development needs.
A discussion on this topic is available here.