Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Idiomatic Rust code guidance based on Apollo GraphQL's best practices handbook for ownership, errors, and performance.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/chapter_01.md
1# Chapter 1 - Coding Styles and Idioms23## 1.1 Borrowing Over Cloning45Rust's ownership system encourages **borrow** (`&T`) instead of **cloning** (`T.clone()`).6> β Performance recommendation78### β When to `Clone`:910* You need to change the object AND preserve the original object (immutable snapshots).11* When you have `Arc` or `Rc` pointers.12* When data is shared across threads, usually `Arc`.13* Avoid massive refactoring of non performance critical code.14* When caching results (dummy example below):15```rust16fn get_config(&self) -> Config {17self.cached_config.clone()18}19```20* When the underlying API expects Owned Data.2122### π¨ `Clone` traps to avoid:2324* Auto-cloning inside loops `.map(|x| x.clone)`, prefer to call `.cloned()` or `.copied()` at the end of the iterator.25* Cloning large data structures like `Vec<T>` or `HashMap<K, V>`.26* Clone because of bad API design instead of adjusting lifetimes.27* Prefer `&[T]` instead of `Vec<T>` or `&Vec<T>`.28* Prefer `&str` or `&String` instead of `String`.29* Prefer `&T` instead of `T`.30* Clone a reference argument, if you need ownership, make it explicit in the arguments for the caller. Example:31```rust32fn take_a_borrow(thing: &Thing) {33let thing_cloned = thing.clone(); // the caller should have passed ownership instead34}35```3637### β Prefer borrowing:38```rust39fn process(name: &str) {40println!("Hello {name}");41}4243let user = String::from("foo");44process(&user);45```4647### β Avoid redundant cloning:48```rust49fn process_string(name: String) {50println!("Hello {name}");51}5253let user = String::from("foo");54process(user.clone()); // Unnecessary clone55```5657## 1.2 When to pass by value? (Copy trait)5859Not all types should be passed by reference (`&T`). If a type is **small** and it is **cheap to copy**, it is often better to **pass it by value**. Rust makes it explicit via the `Copy` trait.6061### β When to pass by value, `Copy`:62* The type **implements** `Copy` (`u32`, `bool`, `f32`, small structs).63* The cost of moving the value is negligible.6465```rust66fn increment(x: u32) -> u32 {67x + 168}6970let num = 1;71let new_num = increment(num); // `num` still usable after this point72```7374### β Which structs should be `Copy`?75* When to consider declaring `Copy` on your own types:76* All fields are `Copy` themselves.77* The struct is `small`, up to 2 (maybe 3) words of memory or 24 bytes (each word is 64 bits/8bytes).78* The struct **represents a "plain data object"**, without resourcing to ownership (no heap allocations. Example: `Vec` and `Strings`).7980β**Rust Arrays are stack allocated.** Which means they can be copied if their underlying type is `Copy`, but this will be allocated in the program stack which can easily become a stack overflow. More on [Chapter 3 - Stack vs Heap](./chapter_03.md#33-stack-vs-heap-be-size-smart)8182For reference, each primitive type size in bytes:8384#### Integers:8586| Type | Size |87|------------- |---------- |88| i8 u8 | 1 byte |89| i16 u16 | 2 bytes |90| i32 u32 | 4 bytes |91| i64 u64 | 8 bytes |92| isize usize | Arch |93| i128 u128 | 16 bytes |9495#### Floating Point:9697| Type | Size |98|---------- |---------- |99| f32 | 4 bytes |100| f64 | 8 bytes |101102103#### Other:104105| Type | Size |106|---------- |---------- |107| bool | 1 byte |108| char | 4 bytes |109110111### β Good struct to derive `Copy`:112```rust113#[derive(Debug, Copy, Clone)]114struct Point {115x: f32,116y: f32,117z: f32118}119```120121### β Bad struct to derive `Copy`:122```rust123#[derive(Debug, Clone)]124struct BadIdea {125age: i32,126name: String, // String is not `Copy`127}128```129130### βWhich Enums should be `Copy`?131* If your enum acts like tags and atoms.132* The enum payloads are all `Copy`.133* **βEnums size are based on their largest element.**134135### β Good Enum to derive136```rust137#[derive(Debug, Copy, Clone)]138enum Direction {139North,140South,141East,142West,143}144```145146## 1.3 Handling `Option<T>` and `Result<T, E>`147Rust 1.65 introduced a better way to safely unpack Option and Result types with the `let Some(x) = β¦ else { β¦ }` or `let Ok(x) = β¦ else { β¦ }` when you have a default `return` value, `continue` or `break` default else case. It allows early returns when the missing case is **expected and normal**, not exceptional.148149### β Cases to use each pattern matching for Option and Return150* Use `match` when you want to pattern match against the inner types `T` and `E`151```rust152match self {153Ok(Direction::South) => { β¦ },154Ok(Direction::North) => { β¦ },155Ok(Direction::East) => { β¦ },156Ok(Direction::West) => { β¦ },157Err(E::One) => { β¦ },158Err(E::Two) => { β¦ },159}160161match self {162Some(3|5) => { β¦ }163Some(x) if x > 10 => { β¦ }164Some(x) => { β¦ }165None => { β¦ }166}167```168169* Use `match` when your type is transformed into something more complex Like `Result<T, E>` becoming `Result<Option<T>, E>`.170```rust171match self {172Ok(t) => Ok(Some(t)),173Err(E::Empty) => Ok(None),174Err(err) => Err(err),175}176```177178* Use `let PATTERN = EXPRESSION else { DIVERGING_CODE; }` when the divergent code doesn't need to know about the failed pattern matches or doesn't need extra computation:179```rust180let Some(&Direction::North) = self.direction.as_ref() else {181return Err(DirectionNotAvailable(self.direction));182}183```184185* Use `let PATTERN = EXPRESSION else { DIVERGING_CODE; }` when you want to break or continue a pattern match186```rust187for x in self {188let Some(x) = x else {189continue;190}191}192```193194* Use `if let PATTERN = EXPRESSION else { DIVERGING_CODE; }` when `DIVERGING_CODE` needs extra computation:195```rust196if let Some(x) = self.next() {197// computation198} else {199// computation when `None/Err` or not matched200}201```202203β**If you don't care about the value of the `Err` case, please use `?` to propagate the `Err` to the caller.**204205### β Bad Option/Return pattern matching:206207* Conversion between Result and Option (prefer `.ok()`,`.ok_or()`, and `ok_or_else()`)208```rust209match self {210Ok(t) => Some(t),211Err(_) => None212}213```214215* `if let PATTERN = EXPRESSION else { DIVERGING_CODE; }` when divergent code is a default or pre-computed value (prefer `let PATTERN = EXPRESSION else { DIVERGING_CODE; }`):216```rust217if let Some(values) = self.next() {218// computation219(Some(..), values)220} else {221(None, Vec::new())222}223```224225* Using `unwrap` or `expect` outside tests:226```rust227let port = config.port.unwrap();228```229230## 1.4 Prevent Early Allocation231232When dealing with functions like `or`, `map_or`, `unwrap_or`, `ok_or`, consider that they have special cases for when memory allocation is required, like creating a new string, creating a collection or even calling functions that manage some state, so they can be replaced with their `_else` counter-part:233234### β Good cases235236```rust237let x = None;238assert_eq!(x.ok_or(ParseError::ValueAbsent), Err(ParseError::ValueAbsent));239240let x = None;241assert_eq!(x.ok_or_else(|| ParseError::ValueAbsent(format!("this is a value {x}"))), Err(ParseError::ValueAbsent));242243244let x: Result<_, &str> = Ok("foo");245assert_eq!(x.map_or(42, |v| v.len()), 3);246247248let x : Result<_, String> = Ok("foo");249assert_eq!(x.map_or_else(|e|format!("Error: {e}"), |v| v.len()), 3);250251let x = "1,2,3,4";252assert_eq!(x.parse_to_option_vec.unwrap_or_else(Vec::new), Ok(vec![1, 2, 3, 4]));253```254255### β Bad cases256257```rust258let x : Result<_, String> = Ok("foo");259assert_eq!(x.map_or(format!("Error with uninformed content"), |v| v.len()), 3);260261let x = "1,2,3,4";262assert_eq!(x.parse_to_option_vec.unwrap_or(Vec::new()), Ok(vec![1, 2, 3, 4])); // could be replaced with `.unwrap_or_default`263264let x = None;265assert_eq!(x.ok_or(ParseError::ValueAbsent(format!("this is a value {x}"))), Err(ParseError::ValueAbsent));266```267268### Mapping Err269270When dealing with Result::Err, sometimes is necessary to log and transform the Err into a more abstract or more detailed error, this can be done with `inspect_err` and `map_err`:271272```rust273let x = Err(ParseError::InvalidContent(...));274275x276.inspect_err(|err| tracing::error!("function_name: {err}"))277.map_err(|err| GeneralError::from(("function_name", err)))?;278```279280## 1.5 Iterator, `.iter` vs `for`281282First we need to understand a basic loop with each one of them. Let's consider the following problem, we need to sum all even numbers between 0 and 10 incremented by 1:283284* `for`:285```rust286let mut sum = 0;287for x in 0..=10 {288if x % 2 == 0 {289sum += x + 1;290}291}292```293294* `iter`:295```rust296let sum: i32 = (0..=10)297.filter(|x| x % 2 == 0)298.map(|x| x + 1)299.sum();300```301302> Both versions do the same thing and are correct and idiomatic, but each shines in different contexts.303304### When to prefer `for` loops305* When you need **early exits** (`break`, `continue`, `return`).306* **Simple iteration** with side-effects (e.g., logging, IO)307* logging can be done correctly in `Iterators` using `inspect` and `inspect_err` functions.308* When readability matters more than simplicity or chaining.309310#### Example:311```rust312for value in &mut value {313if *value == 0 {314break;315}316*value += fancy_equation();317}318```319320### When to prefer `iterators` loops (`.iter()` and `.into_iter()`)321* When you are `transforming collections` or `Option/Results`.322* You can **compose multiple steps** elegantly.323* No need for early exits.324* You need support for indexed values with `.enumerate`.325```rust326let values: Vec<_> = vec.into_iter()327.enumerate()328.filter(|(_index, value)| value % 2 == 0)329.map(|(index, value)| value % index)330.collect()331```332* You need to use collections functions like `.windows` or `chunks`.333* You need to combine data from multiple sources and don't want to allocate multiple collections.334* Iterators can be combined with `for` loops:335```rust336for value in vec.iter().enumerate()337.filter(|(index, value)| value % index == 0) {338// ...339}340```341342> #### βREMEMBER: Iterators are Lazy343>344> * `.iter`, `.map`, `.filter` don't do anything until you call its consumer, e.g. `.collect`, `.sum`, `.for_each`.345> * **Lazy Evaluation** means that iterator chains are fused into one loop at compile time.346347### π¨ Anti-patterns to AVOID348349* Don't chain without formatting. Prefer each chained function on its own line with the correct indentation (`rustfmt` should take care of this).350* Don't chain if it makes the code unreadable.351* Avoid needlessly collect/allocate of a collection (e.g. vector) just to throw it away later by some larger operation or by another iteration.352* Prefer `iter` over `into_iter` unless you don't need the ownership of the collection.353* Prefer `iter` over `into_iter` for collections that inner type implements `Copy`, e.g. `Vec<i32>`.354* For summing numbers prefer `.sum` over `.fold`. `.sum` is specialized for summing values, so the compiler knows it can make optimizations on that front, while fold has a blackbox closure that needs to be applied at every step. If you need to sum by an initial value, just added in the expression `let my_sum = [1, 2, 3].sum() + 3`.355356## 1.6 Comments: Context, not Clutter357358> "Context are for why, not what or how"359360Well-written Rust code, with expressive types and good naming, often speaks for itself. Many high-quality codebases thrive on **few or no comments**. And that's a good thing.361362Still, there are **moments where code alone isn't enough** - when there are performance quirks, external constraints, or non-obvious tradeoffs that require a nudge to the reader. In those cases, a concise comment can prevent hours of head-scratching or searching git history.363364### β Good comments365366* Safety concerns:367```rust368// SAFETY: We have checked that the pointer is valid and non-null. @Function xyz.369unsafe { std::ptr::copy_nonoverlapping(src, dst, len); }370```371372* Performance quirks:373```rust374// This algorithm is a fast square root approximation375const THREE_HALVES: f32 = 1.5;376fn q_rsqrt(number: f32 ) -> f32 {377let mut i: i32 = number.to_bits() as i32;378i = 0x5F375A86_i32.wrapping_sub(i >> 1);379let y = f32::from_bits(i as u32);380y * (THREE_HALVES - (number * 0.5 * y * y))381}382```383384* Clear code beats comments. However, when the why isn't obvious, say it plainly - or link to where:385```rust386// PERF: Generating the root store per subgraph caused high TLS startup latency on MacOS387// This works as a caching alternative. See: [ADR-123](link/to/adr-123)388let subgraph_tls_root_store: RootCertStore = configuration389.tls390.subgraph391.all392.create_certificate_store()393.transpose()?394.unwrap_or_else(crate::services::http::HttpClientService::native_roots_store);395```396397### β Bad comments398399* Wall-of-text explanations: long comments and multiline comments400```rust401// Lorem Ipsum is simply dummy text of the printing and typesetting industry.402// Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,403// when an unknown printer took a galley404fn do_something_odd() {405β¦406}407```408> Prefer `/// doc` comment if it's describing the function.409410* Comments that could be better represented as functions or are plain obvious411```rust412fn computation() {413// increment i by 1414i += 1;415}416```417418### β Breaking up long functions over commenting them419420If you find yourself writing a long comment explaining "what", "how" or "each step" in a function, it might be time to split it. So the suggestion is to refactor. This can be beneficial not only for readability, but testability:421422#### β Instead of:423```rust424fn process_request(request: T) {425// We first need to validate request, because of corner case x, y, z426// As the payload can only be decoded when they are valid427// Then we can perform authorization on the payload428// lastly with the authorized payload we can dispatch to handler429}430```431432#### β Prefer433```rust434fn process_request(request: T) -> Result<(), Error> {435validate_request_headers(&request)?;436let payload = decode_payload(&request);437authorize(&payload)?;438dispatch_to_handler(payload)439}440441#[cfg(test)]442mod tests {443#[test]444fn validate_request_happy_path() { ... }445446#[test]447fn validate_request_fails_on_x() { ... }448449#[test]450fn validate_request_fails_on_y() { ... }451452#[test]453fn decode_validated_request() { ... }454455#[test]456fn authorize_payload_xyz() { ... }457}458```459460Let **structure** and **naming** replace commentary, and enhance its documentation with **tests as living documentation**.461462### π TODOs are not comments - track them properly463464Avoid leaving lingering `// TODO: Lorem Ipsum` comments in the code. Instead:465* Turn them into Jira or Github Issues.466* If needed, to avoid future confusion, reference the issue in the code and the code in the issue.467468```rust469// See issue #123: support hyper 2.0470```471472This helps keeping the code clean and making sure tasks are not forgotten.473474### Comments as Living Documentation475476There are a few gotchas when calling comments "living documentation":477* Code evolves.478* Context changes.479* Comments get stale.480* Many large comments make people avoid reading them.481* Team becomes fearful of delete irrelevant comments.482483If you find a comment, **don't trust it blindly**. Read it in context. If it's wrong or outdated, fix or remove it. A misleading comment is worse than no comments at all.484485> Comments should bother you - they demand re-verification, just like stale tests.486487When deeper justification is needed, prefer to:488* **Link to a Design Doc or an ADR**, business logic lives well in design docs while performance tradeoffs live well in ADRs.489* Move runtime example and usage docs into Rust Docs, `/// doc comment`, where they can be tested and kept up-to-date by tools like `cargo doc`.490491> Doc-comments and Doc-testing, `///` and `//!` in [Chapter 8 - Comments vs Documentation](./chapter_08.md)492493## 1.7 Use Declarations - "imports"494495Different languages have different ways of sorting their imports, in the Rust ecosystem the [standard way](https://github.com/rust-lang/rustfmt/issues/4107) is:496497- `std` (`core`, `alloc` would also fit here).498- External crates (what is in your Cargo.toml `[dependencies]`).499- Workspace crates (workspace member crates).500- This module `super::`.501- This module `crate::`.502503```rust504// std505use std::sync::Arc;506507// external crates508use chrono::Utc;509use juniper::{FieldError, FieldResult};510use uuid::Uuid;511512// crate code lives in workspace513use broker::database::PooledConnection;514515// super:: / crate::516use super::schema::{Context, Payload};517use super::update::convert_publish_payload;518use crate::models::Event;519```520521Some enterprise solutions opt to include their core packages after `std`, so all external packages that start with enterprise name are located before the others:522523```rust524// std525use std::sync::Arc;526527// enterprise external crates528use enterprise_crate_name::some_module::SomeThing;529530// external crates531use chrono::Utc;532use juniper::{FieldError, FieldResult};533use uuid::Uuid;534535// crate code lives in workspace536use broker::database::PooledConnection;537538// super:: / crate::539use super::schema::{Context, Payload};540use super::update::convert_publish_payload;541use crate::models::Event;542```543544One way of not having to manually control this is using the following arguments in your `rustfmt.toml`:545546```toml547reorder_imports = true548imports_granularity = "Crate"549group_imports = "StdExternalCrate"550```551552> As of Rust version 1.88, it is necessary to execute rustfmt in nightly to correctly reorder code `cargo +nightly fmt`.553