Where the Analogy Ends
Last updated on 2026-09-25 | Edit this page
Overview
Questions
- What problems are ownership and borrowing designed to prevent?
- How do traits and enums shape Rust APIs?
- What does âfearless concurrencyâ mean in practice?
- Do Rustâs guarantees leave room for exploratory code?
Objectives
- Explain ownership, borrowing, and lifetimes without relying on Python analogies.
- Distinguish Rust enums and traits from superficially similar Python features.
- Connect compile-time checks to safe resource use and concurrency.
- Separate prototyping shortcuts from production error and ownership policies.
Ownership is a resource model
Every Rust value has an owner. When that owner goes out of scope, the value is dropped. A value can move to a new owner, be borrowed immutably by many readers, or be borrowed mutably by one writer. These rules prevent use-after-free and data races before the program runs.
RUST
fn label_length(label: &str) -> usize {
label.len()
}
fn main() {
let label = String::from("second breakfast");
let length = label_length(&label);
println!("{label} has {length} bytes");
}
label_length borrows the string, so label
remains available to its owner.
When the borrow checker objects, first ask what the callee needs to do. The answer usually selects the parameter shape:
| Calleeâs intent | Typical parameter | Consequence for the caller |
|---|---|---|
| Read for the duration of the call | &T |
The caller keeps ownership; many immutable borrows may coexist. |
| Change the callerâs value | &mut T |
The caller keeps ownership; only one mutable borrow may exist at a time. |
| Store or consume the value | T |
Ownership moves unless the type implements
Copy. |
| Work on an independent duplicate |
T from value.clone()
|
The caller keeps the original and pays the cloneâs explicit cost. |
Start with the least authority the function needs. A validator that
only reads text should accept &str; taking a
String would force ownership transfer or an unnecessary
clone.
Lifetimes describe relationships
Most lifetimes are inferred. When a function returns a reference, an explicit lifetime may be needed to state which input keeps that reference valid.
RUST
fn choose_longer<'a>(left: &'a str, right: &'a str) -> &'a str {
if left.len() >= right.len() {
left
} else {
right
}
}
The annotation does not extend either valueâs lifetime. It gives the compiler a relationship it can check.
Enums carry data; traits describe behavior
Rust enums model a closed set of alternatives, and each variant may carry different data. Pattern matching ensures that every case is considered. Traits define shared behavior and can be used for generics or dynamic dispatch.
acorn-schema makes both ideas concrete. A patent is one
type with distinct granted, application, and publication variants. The
acorn-py binding matches those variants and constructs one
consistent Python-facing Patent object.
RUST
use acorn_schema::pid::patent::{CountryCode, KindCode};
use acorn_schema::pid::{Patent, PersistentIdentifier};
fn kind(patent: &Patent) -> &'static str {
match patent {
| Patent::Granted { .. } => "grant",
| Patent::Application { .. } => "application",
| Patent::Publication { .. } => "publication",
}
}
fn main() {
let patent = Patent::Granted {
country_code: Some(CountryCode::US),
kind_code: KindCode::B2,
serial_number: "7654321".to_string(),
};
assert_eq!(kind(&patent), "grant");
assert_eq!(patent.identifier(), "US 7654321 B2");
}
This is the schema crateâs real Patent enum rather than
a workshop-only facsimile. Importing PersistentIdentifier
brings its shared methods into scope; ACORNâs identifier types use that
trait for behaviors such as identifier() and
schema_uri(). The exhaustive match must be
revisited if the schema adds another patent variant.
Compile-time guarantees and concurrency
The same ownership rules apply across threads. A value cannot be mutated from multiple places unless its type provides safe synchronization. Data races are far harder to express, although other concurrency bugs remain possible.
Scoped threads can safely borrow inputs because Rust proves that every worker finishes before the scope ends. Each worker below computes its own value, and the parent combines the results instead of sharing mutable state:
RUST
use std::thread;
fn count_nonempty(left: &[String], right: &[String]) -> Result<usize, &'static str> {
thread::scope(|scope| {
let left_worker =
scope.spawn(|| left.iter().filter(|value| !value.is_empty()).count());
let right_worker =
scope.spawn(|| right.iter().filter(|value| !value.is_empty()).count());
match (left_worker.join(), right_worker.join()) {
| (Ok(left_count), Ok(right_count)) => Ok(left_count + right_count),
| _ => Err("a counting worker panicked"),
}
})
}
The slices are borrowed, not copied. Returning partial counts also
avoids a mutex. Ownership rules prevent either worker from outliving the
borrowed data, while the explicit Result records that a
worker may panic. Rust still cannot prevent deadlocks, starvation, or a
logically incorrect division of work.
đ˘ Typical misbeliefs
Rust prototypes do not have to look like finished library code. These beliefs usually come from treating every early decision as permanent:
| Misbelief | A more useful working assumption |
|---|---|
| âMemory safety and prototyping just donât go together.â | Compiler feedback can be part of the experiment. It rules out invalid memory relationships while you test the idea. |
| âOwnership and borrowing take the fun out of prototyping.â | They can interrupt a first draft, so begin with owned values and use a temporary clone when that keeps the experiment moving. Revisit the ownership once the shape is clear. |
| âYou have to get all the details right from the beginning.â | Type inference, concrete types, and
todo!() let you postpone decisions without pretending the
unfinished path works. |
| âRust always requires you to handle errors.â | Rust makes recoverable failure visible with
Result, but a prototype can deliberately stop with
unwrap(), todo!(), or
unreachable!(). Production code still needs an intentional
policy for user-triggerable failures. |
The prototypeâs job is to answer a question. Rust makes many shortcuts visible, which gives us a practical list to revisit before shipping.
- Ownership determines who is responsible for a value and when it is released.
- Borrowing provides temporary access without transferring ownership.
- Lifetimes let the compiler verify relationships between references.
- Enums, traits, and concurrency checks are central Rust design tools.
- Prototypes may defer decisions, provided their shortcuts remain visible and are reviewed before production.