
Rust is a hot topic in the field of language design. It allows us to build efficient, memory-safe programs with concise, portable, and sometimes even beautiful code.
However, everything has two sides; it’s not all roses and sunshine. The details of memory management often drive development work into madness and make the code uglier and more repetitive than in “high-level” programming languages like Haskell or OCaml. The most frustrating part is that in almost all cases, these issues are not defects of the compiler, but direct consequences of design choices made by the Rust team.
In the book “Elements of Programming”, author Alexander Stepanov wrote: “Functional programming deals with values; imperative programming deals with objects.” This article details, through rich examples, how frustrating it can be for developers to handle Rust with a functional programming mindset, and why Rust has no other choice. It is recommended to bookmark.

Objects and References: The Root of All Evil
Values and objects serve complementary roles. Values are immutable and independent of any specific implementation in the computer. Objects are mutable and have computer-specific implementations.
——Alexander Stepanov, “Elements of Programming”
Before diving deep into Rust, it is helpful to understand the differences between objects, values, and references.
In the context of this article, values are entities with different identities, such as numbers and strings. Objects are representations of values in computer memory. References are addresses of objects that we can use to access an object or part of it.

System programming languages, like C++ and Rust, force programmers to deal with the distinction between objects and references. This distinction enables us to write incredibly fast code, but at a high cost: it is an endless source of bugs. If other parts of the program reference an object, modifying the contents of that object is almost always an error. There are multiple ways to address this issue:
-
Ignore the problem and trust the programmer’s actions. Most traditional system programming languages, like C++, have taken this path.
-
Make all objects immutable. This option is the foundation of pure functional programming techniques in Haskell and Clojure.
-
Completely prohibit references. The Val language explored this programming style.
-
Adopt a type system that prevents the modification of referenced objects. Languages like ATS and Rust have chosen this path.
The distinction between objects and references is also the root of accidental complexity and explosion of choices. A language with immutable objects and automatic memory management accommodates developers’ blind spots regarding this distinction and treats everything as a value (at least in pure code). A unified storage model liberates programmers’ thinking energy, allowing them to write more expressive and elegant code.
However, what we gain in convenience, we lose in efficiency: pure functional programs often require more memory, may become unresponsive, and are harder to optimize, which means projects can be rushed.

Internal Injury 1: Abstractions Full of Holes
Manual memory management and ownership-aware type systems interfere with our ability to abstract code into smaller parts.
Common Expression Elimination
Extracting common expressions into variables can introduce unexpected challenges. Let’s start with the following code snippet.
let x = f("a very long string".to_string());
let y = g("a very long string".to_string());
// …
Swipe left and right to view the complete code
As above, “a very long string”.to_string(), our first instinct is to name the expression and use it twice:
let s = "a very long string".to_string();
let x = f(s);
let y = g(s);
Swipe left and right to view the complete code
However, our first prototype version will not compile because the String type does not implement the Copy trait. We must instead use the following expression:
let s = "a very long string".to_string();
f(s.clone());
g(s);
Swipe left and right to view the complete code
If we care about the extra memory allocation, because copying memory becomes explicit, we can see the additional verbosity from a positive angle. But in practice, this can be annoying, especially when you add
h(s);
let s = "a very long string".to_string();
f(s.clone());
g(s);
// fifty lines of code...
h(s); // ← won’t compile, you need scroll up and update g(s).
Swipe left and right to view the complete code

In Rust, let x = y; does not mean x and y are the same. A naturally broken example is when y is an overloaded function, this natural property will break. For instance, let’s define a short name for an overloaded function.
// Do we have to type "MyType::from" every time?
// How about introducing an alias?
let x = MyType::from(b"bytes");
let y = MyType::from("string");
// Nope, Rust won't let us.
let f = MyType::from;
let x = f(b"bytes");
let y = f("string");
// - ^^^^^^^^ expected slice `[u8]`, found `str`
// |
// arguments to this function are incorrect
Swipe left and right to view the complete code
This code snippet does not compile because the compiler binds f to a specific instance of MyType::from rather than a polymorphic function. We must explicitly make f polymorphic.
// Compiles fine, but is longer than the original.
fn f<T: Into<MyType>>(t: T) -> MyType { t.into() }
let x = f(b"bytes");
let y = f("string");
Swipe left and right to view the complete code
Haskell programmers may find this problem familiar: it looks suspiciously like the terrible monomorphism restriction! Unfortunately, rustc doesn’t have a NoMonomorphismRestriction field.

Breaking code into functions may be more difficult than expected because the compiler cannot reason about aliasing across function boundaries. Suppose we have the following code.
impl State {
fn tick(&mut self) {
self.state = match self.state {
Ping(s) => { self.x += 1; Pong(s) }
Pong(s) => { self.x += 1; Ping(s) }
}
}
}
Swipe left and right to view the complete code
self.x += 1 appears multiple times. Why not extract it into a method…
impl State {
fn tick(&mut self) {
self.state = match self.state {
Ping(s) => { self.inc(); Pong(s) } // ← compile error
Pong(s) => { self.inc(); Ping(s) } // ← compile error
}
}
fn inc(&mut self) {
self.x += 1;
}
}
Swipe left and right to view the complete code
Rust will yell at us because the method tries to reborrow self.state exclusively while the surrounding context still holds a mutable reference to self.state.
The Rust 2021 edition implemented non-overlapping captures to solve similar problems with closures. Prior to Rust 2021, code like x.f.m(||x.y) might not compile, but could be manually inlined, and closures could solve that error. For example, suppose we have a structure that owns a map and default values for map entries.
struct S { map: HashMap<i64, String>, def: String }
impl S {
fn ensure_has_entry(&mut self, key: i64) {
// Doesn't compile with Rust 2018:
self.map.entry(key).or_insert_with(|| self.def.clone());
// | ------ -------------- ^^ ---- second borrow occurs...
// | | | |
// | | | |
// | | mutable borrow later used by call
// | mutable borrow occurs here
}
}
Swipe left and right to view the complete code
However, if we inline the definition of or_insert_with and the lambda function, the compiler can finally see that the borrowing rules hold
struct S { map: HashMap<i64, String>, def: String }
impl S {
fn ensure_has_entry(&mut self, key: i64) {
use std::collections::hash_map::Entry::*;
// This version is more verbose, but it works with Rust 2018.
match self.map.entry(key) {
Occupied(mut e) => e.get_mut(),
Vacant(mut e) => e.insert(self.def.clone()),
};
}
}
Swipe left and right to view the complete code
When someone asks you, “What can Rust closures do that named functions cannot?” you will know the answer: they can only capture the fields they use.

The newtype idiom in Rust allows programmers to give a new identity to existing types. The name of this idiom comes from the newtype keyword in Haskell.
A common use of this idiom is to handle isolated rules and define trait implementations for alias types. For example, the following code defines a new type that displays a byte vector in hexadecimal.
struct Hex(Vec<u8>);
impl std::fmt::Display for Hex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_'>) -> std::fmt::Result {
self.0.iter().try_for_each(|b| write!(f, "{:02x}", b))
}
}
println!("{}", Hex((0..32).collect()));
// => 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
Swipe left and right to view the complete code
The newtype idiom is problematic: the representation of the hexadecimal type in machine memory is the same as that of Vec. However, despite having the same representation, the compiler does not treat our new type as a strong alias of Vec. For instance, we cannot safely convert Vec to Vec> and return it without reallocating the outward vector. Additionally, we cannot safely coerce &Vec into &Hex without copying the bytes.
fn complex_function(bytes: &Vec<u8>) {
// … a lot of code …
println!("{}", &Hex(bytes)); // That does not work.
println!("{}", Hex(bytes.clone())); // That works but is slow.
// … a lot of code …
}
Swipe left and right to view the complete code
In summary, the newtype idiom is a leaky abstraction because it is a convention rather than a first-class language feature.

Whenever a programmer describes structure fields or passes parameters to functions, she must decide whether the field/parameter is an object or a reference. Or is the best choice to decide at runtime? That’s a lot of decisions! Unfortunately, sometimes there is no best choice. In such cases, we grit our teeth and define several versions of the same type with slightly different field types.
Most functions in Rust take parameters by reference and return results as self-contained objects. This pattern is very common, so it may help to define new terms. I call the input types with lifetime parameters views because they are best for checking data. I call regular output types bundles because they are independent.
The following code snippet comes from the Lucet WebAssembly runtime.
/// A WebAssembly global along with its export specification.
/// The lifetime parameter exists to support zero-copy deserialization
/// for the `&str` fields at the leaves of the structure.
/// For a variant with owned types at the leaves, see `OwnedGlobalSpec`.
pub struct GlobalSpec<'a> {
global: Global<'a>,
export_names: Vec<&'a str>,
}
…
/// A variant of `GlobalSpec` with owned strings throughout.
/// This type is useful when directly building up a value to be serialized.
pub struct OwnedGlobalSpec {
global: OwnedGlobal,
export_names: Vec<String>,
}
Swipe left and right to view the complete code
The author copied the GlobalSpec data structure to support two use cases:
-
GlobalSpec<a> is a view object parsed by the code author from a byte buffer. The various fields of this view point to the relevant regions of the buffer. This representation is useful for functions that need to inspect values of type GlobalSpec without modifying them.
-
OwnedGlobalSpec is a bundle: it does not contain references to other data structures. This representation is useful for constructing values of type GlobalSpec and passing them or putting them in containers.
In languages with automatic memory management, we could combine the efficiency of GlobalSpec<a> with the versatility of OwnedGlobalSpec in a single type declaration.

Internal Injury 2: Composition Becomes “Asceticism”
In Rust, composing programs from smaller parts can be incredibly frustrating.

When developers have two different objects, they often want to combine them into a structure. Sounds simple? Not so easy in Rust.
Suppose we have an object Db that has a method to provide you with another object Snapshot<a>. The lifetime of the snapshot depends on the lifetime of the database.
struct Db { /* … */ }
struct Snapshot<'a> { /* … */ }
impl Db { fn snapshot<'a>(&'a self) -> Snapshot<'a>; }
Swipe left and right to view the complete code
We might want to bundle the database with its snapshot, but Rust does not allow this.
// There is no way to define the following struct without
// contaminating it with lifetimes.
struct DbSnapshot {
snapshot: Snapshot<'a>, // what should 'a be?
db: Arc<Db>,
}
Swipe left and right to view the complete code
Rust enthusiasts call this arrangement “brother pointers.” Rust prohibits brother pointers in safe code because they break Rust’s safety model.
As discussed in the section on objects, values, and references, modifying referenced objects is often a bug. In our example, the snapshot object may depend on the physical location of the db object. If we move DbSnapshot as a whole, the physical location of the db field will change, potentially damaging the references in the snapshot object. We know that moving Arc<Db> will not change the location of the Db object, but we cannot pass this information to rustc.
Another issue with DbSnapshot is that the order of destruction of its fields is important. If Rust allowed sibling pointers, changing the field order could introduce undefined behavior: the destructor of the snapshot may try to access fields of the db object that have already been destroyed.

Cannot Pattern Match on Boxes
In Rust, we cannot pattern match on boxed types like Box, Arc, String, and Vec. This limitation often breaks trades because we cannot avoid boxing when defining recursive data types.
For example, let us try to match a vector of strings.
let x = vec!["a".to_string(), "b".to_string()];
match x {
// - help: consider slicing here: `x[..]`
["a", "b"] => println!("OK"),
// ^^^^^^^^^^ pattern cannot match with input type `Vec<String>`
_ => (),
}
Swipe left and right to view the complete code
First, we cannot match a vector; we can only match a slice. Fortunately, the compiler suggests a simple solution: we must replace x with x[..] in the match expression. Let’s try it.
let x = vec!["a".to_string(), "b".to_string()];
match x[..] {
// ----- this expression has type `[String]`
["a", "b"] => println!("OK"),
// ^^^ expected struct `String`, found `&str`
_ => (),
}
Swipe left and right to view the complete code
As you can see, removing one layer of boxing is not enough to satisfy the compiler. We also need to unbox the strings within the vector, which is not possible without allocating a new vector:
let x = vec!["a".to_string(), "b".to_string()];
// We have to allocate new storage.
let x_for_match: Vec<_> = x.iter().map(|s| s.as_str()).collect();
match &x_for_match[..] {
["a", "b"] => println!("OK"), // this compiles
_ => (),
}
Forget about balancing Red-Black trees in five lines of code in Rust
Swipe left and right to view the complete code
Forget about balancing Red-Black trees in five lines of code in Rust.
Honestly, give up on balancing Red-Black trees in Rust with five lines of code!

Rust uses orphan rules to decide whether a type can implement a trait. For non-generic types, these rules prohibit implementing a trait for a type outside the crate that defines the trait or type. In other words, the crate that defines the trait must depend on the crate that defines the type, and vice versa.

These rules make it easy for the compiler to guarantee coherence, which is a clever way of saying that all parts of the program see the same trait implementation for a specific type. In exchange, this rule complicates integrating traits and types from unrelated libraries.
An example is traits we only want to use in tests, such as arbitrary traits in the proptest crate. If the compiler derives implementations for types from our crate, we can save a lot of types, but we want our production code to be independent of the proptest crate. In a perfect setup, all arbitrary implementations would go into a separate test-only crate. Unfortunately, the orphan rules oppose this arrangement, forcing us to grit our teeth and manually write proptest strategies.
Under the orphan rules, type conversion traits (like From and Into) also have issues. I often see xxx type crates start small but eventually become bottlenecks in the compilation chain. Splitting such crates into smaller parts is often daunting because complex conversion networks connect distant types. The orphan rules do not allow us to cut these crates on module boundaries and move all conversions to a separate crate without doing a lot of tedious work.
Do not get me wrong: orphan rules are a default principle. Haskell allows you to define orphan instances, but programmers do not favor this practice. It saddens me to be unable to escape orphan rules. In large codebases, breaking large crates into smaller parts and maintaining a shallow dependency graph is the only way to achieve acceptable compilation speed. Orphan rules often hinder trimming the dependency graph.

Fearless Concurrency is a Lie
The Rust team coined the term Fearless Concurrency to indicate that Rust can help you avoid common pitfalls associated with parallel and concurrent programming. Despite these claims, every time I introduce concurrency into a Rust program, my cortisol levels rise.

Thus, for Safe Rust programs, it is perfectly “fine” if synchronizations are incorrect, leading to deadlocks or doing something meaningless. Clearly, such programs are not good, but Rust can only hold your hand
——The Rustonomicon, Data Races and Race Conditions
Safe Rust prevents a specific type of concurrency error called data races. However, concurrent Rust programs can still run incorrectly in many other ways.
A class of concurrency errors I have personally experienced is deadlocks. A typical explanation of such errors includes two locks and two processes trying to acquire the locks in opposite order. However, if the locks you use are not reentrant (Rust’s locks are not), then one lock is enough to cause a deadlock.
For example, the following code is incorrect because it tries to acquire the same lock twice. If do_something and helper_function are large and far apart in the source file, or if we call helper_function on a rare execution path, it may be difficult to spot this bug.
impl Service {
pub fn do_something(&self) {
let guard = self.lock.read();
// …
self.helper_function(); // BUG: will panic or deadlock
// …
}
fn helper_function(&self) {
let guard = self.lock.read();
// …
}
}
Swipe left and right to view the complete code
RwLock::read documentation mentions that if the current thread already holds the lock, the function may deadlock. I just got a hanging program.
Some languages attempt to provide solutions to this issue in their concurrency toolkit. The Clang compiler has thread safety annotations that support a form of static analysis that can detect race conditions and deadlocks. However, the best way to avoid deadlocks is not to use locks. Two fundamentally different techniques that solve this problem are Software Transactional Memory (implemented in Haskell, Clojure, and Scala) and the actor model (Erlang was the first language to fully adopt it).

File Systems are Shared Resources
Rust provides us with powerful tools to handle shared memory. However, once our programs need to interact with the outside world (e.g., using network interfaces or file systems), we are on our own.
Rust is similar to most modern languages in this respect. However, it gives you a false sense of security.
Be careful, even in Rust, paths are raw pointers. Most file operations are essentially unsafe, and if file access is not synchronized correctly, data races (in a broad sense) can occur. For example, as of February 2023, I still encountered a concurrency bug in rustup (https://rustup.rs/) that lasted six years (https://github.com/rust-lang/rustup/issues/988).

Implicit Asynchronous Runtime
I cannot seriously believe in quantum theory, because physics should describe the existence in spacetime without “incredible spooky action at a distance”.
One of my favorite things about Rust is its focus on local reasoning. Looking at the type signature of a function often gives you a thorough understanding of its functionality.
-
State mutations are explicit due to mutability and lifetime annotations.
-
Error handling is explicit and intuitive due to the ubiquitous Result type.
-
If used correctly, these features often lead to mysterious compilation effects.
However, asynchronous programming in Rust is different.
Rust supports async/.await syntax to define and compose asynchronous functions, but runtime support is limited. Several libraries (referred to as asynchronous runtimes) define asynchronous functions that interact with the operating system. The tokio package is the most popular library.
A common problem with runtimes is that they rely on implicit parameter passing. For example, the tokio runtime allows concurrent tasks to be spawned at any point in the program. For this function to work, the programmer must pre-construct a runtime object.
fn innocently_looking_function() {
tokio::spawn(some_async_func());
// ^
// |
// This code will panic if we remove this line. Spukhafte Fernwirkung!
} // |
// |
fn main() { // v
let _rt = tokio::runtime::Runtime::new().unwrap();
innocently_looking_function();
}
Swipe left and right to view the complete code
These implicit parameters turn compile-time errors into runtime errors. What should have been a compile-time error becomes a “debugging adventure”:
-
If the runtime is an explicit parameter, the code will not compile unless the programmer constructs a runtime and passes it as a parameter. When the runtime is implicit, your code may compile fine, but if you forget to annotate the main function with a magical macro, it will crash at runtime.
-
Mixing libraries with different runtimes is very complicated. If this issue involves multiple major versions of the same runtime, the problem becomes even more confusing. My experience writing asynchronous Rust code can be described as a tragic “accident”!
Some may argue that using ubiquitous parameters throughout the call stack is illogical. Explicitly passing all parameters is the only way to scale well.

In 2015, Bob Nystrom stated in his blog “What Color is Your Function”: Rational people might think that languages hate us.
Rust’s async/.await syntax simplifies the encapsulation of asynchronous algorithms, but it also introduces a fair amount of complexity issues: coloring each function blue (synchronous) or red (asynchronous). New rules need to be followed:
-
Synchronous functions can call other synchronous functions and get results. Asynchronous functions can call and .await other asynchronous functions to get results.
-
We cannot directly call and await asynchronous functions from synchronous functions. We need an asynchronous runtime that will execute an asynchronous function for us.
-
We can call synchronous functions from asynchronous functions. But be careful! Not all synchronous functions are the same shade of blue.
Indeed, some synchronous functions magically turn purple: they can read files, connect threads, or sleep the thread::sleep on the couch. We do not want to call these purple (blocking) functions from red (asynchronous) functions because they would block the runtime and kill the performance advantage that drives us into asynchronous chaos.
Unfortunately, purple functions are very tricky: it is impossible to determine if a function is purple without checking the body of the function and the bodies of all other functions in the call graph. These bodies are also evolving, so we better pay attention to them.
The real fun comes from having a codebase with shared ownership where multiple teams squeeze synchronous and asynchronous code together. Such packages often become bug silos, waiting for enough system load to reveal another purple defect in the sandwich, making the system unresponsive.
Languages built around green threads, like Haskell and Go, eliminate the proliferation of function colors. In these languages, building concurrent programs from independent components is easier and safer.

Bjarne Stroustrup, the father of C++, once said that there are only two kinds of languages in the world: one that people always complain about and another that no one uses.
Rust is a “disciplined” language that gets many important decisions right, such as uncompromising attention to safety, trait system design, lack of implicit conversions, and overall approach to error handling. It allows us to relatively quickly develop robust and memory-safe programs without sacrificing execution speed.
However, I often find myself overwhelmed by unexpected complexity, especially when I care less about performance and want to get things done quickly (like in test code). Rust breaks programs into smaller parts and composes them from smaller parts. Moreover, Rust only partially eliminates concurrency issues.
In the end, I just want to say that no language is a panacea.
Original link:
https://mmapped.blog/posts/15-when-rust-hurts.html#filesystem-shared-resource

●30-Year-Old Ruby: Why Is It Hard to Stand Out After Challenging Java?
●Billions of Downloaded Projects Face Maintenance Dilemmas! The Person in Charge Complains: Open Source Has Been Ruined to the Point of No One Paying!
Three Clicks to Give the Editor a Chicken Leg!