diff --git a/skills/writing-rust/SKILL.md b/skills/writing-rust/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..2caded582b1a3fdedd69493800ab76f03819214c --- /dev/null +++ b/skills/writing-rust/SKILL.md @@ -0,0 +1,147 @@ +--- +name: writing-rust +description: Writes idiomatic, well-structured Rust code following community conventions and best practices. Use when working on Rust projects, writing Rust code, debugging Rust errors, or when the user mentions Rust, Cargo, or crates. +license: AGPL-3.0-or-later +metadata: + author: Amolith +--- + +Write idiomatic Rust. All code must compile, pass clippy, and have tests. + +## Code quality checks + +Run these in order before considering any code complete. Never submit code that fails any check. + +```bash +cargo fmt +cargo check +cargo clippy -- -D warnings +cargo test +``` + +## Project structure + +``` +project-root/ +├── Cargo.toml +├── Cargo.lock +├── src/ +│ ├── main.rs # Binary entry point (if applicable) +│ ├── lib.rs # Library root (if applicable) +│ └── module_name/ +│ ├── mod.rs +│ └── submodule.rs +├── tests/ # Integration tests +├── benches/ # Benchmarks (if applicable) +└── examples/ # Example usage (if applicable) +``` + +## Naming conventions + +| Item | Convention | Example | +|------------------|-------------------|----------------------------| +| Crates | `snake_case` | `my_crate` | +| Modules | `snake_case` | `my_module` | +| Types / Traits | `PascalCase` | `MyStruct`, `MyTrait` | +| Functions | `snake_case` | `do_something()` | +| Constants | `SCREAMING_SNAKE` | `MAX_RETRIES` | +| Local variables | `snake_case` | `item_count` | +| Type parameters | Single uppercase | `T`, `E`, `K`, `V` | +| Lifetimes | Short lowercase | `'a`, `'de` | + +## Error handling + +- Prefer `Result` over `panic!()` for recoverable errors. +- Use `thiserror` for library error types, `anyhow` for application-level errors. +- Avoid `.unwrap()` and `.expect()` in production code. Propagate with `?`. +- Define custom error types when a module has more than one failure mode. +- When a fallible function consumes an argument, return it inside the error type so callers can recover it. + +## Ownership and borrowing + +- Prefer borrowing (`&T`, `&mut T`) over transferring ownership when the caller doesn't need to give up the value. +- Use `Clone` sparingly — never just to satisfy the borrow checker. +- Prefer `&str` over `String`, `&[T]` over `&Vec`, and `&T` over `&Box` in function parameters. +- Use `Cow<'_, str>` when a function may or may not need to allocate. +- Use `mem::take` or `mem::replace` to move values out of mutable references without cloning. + +## Structs and enums + +- Derive common traits where appropriate: `Debug`, `Clone`, `PartialEq`, `Eq`, `Hash`, `Default`. +- Use the builder pattern for structs with many optional fields. +- Prefer enums over boolean flags for state representation. +- Use `#[non_exhaustive]` on public structs and enums that may grow over time. +- Use the newtype pattern for type safety wrappers around primitives (zero-cost abstraction). +- Decompose large structs into smaller ones when the borrow checker prevents independent field access. + +## Testing + +- All implemented functionality must have corresponding tests. +- Tests must validate behavior, not just assert `true`. +- Use descriptive test names that explain what is being tested. +- Test both the happy path and error/edge cases. + +```bash +cargo test # Run all tests +cargo test test_name # Run a specific test +cargo test -- --nocapture # Run tests with output +``` + +## Design principles + +- **KISS**: Simplicity should be a key goal in design. +- **YAGNI**: Don't add functionality until it is necessary. +- **DRY**: Every piece of knowledge should have a single, authoritative representation. +- **SOLID**: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion. +- **Composition over inheritance**: Favour traits and composition over emulating inheritance. +- **Command-Query Separation**: Functions should either return data or produce side effects, not both. +- **Principle of Least Astonishment**: Components should behave the way most users expect. + +## Performance + +- Avoid unnecessary heap allocations. Prefer stack allocation and slices. +- Use `&[T]` instead of `&Vec` and `&str` instead of `&String` in function signatures. +- Profile before optimizing. Use `cargo bench` and tools like `criterion`. +- Stay lazy with iterators. Avoid `.collect()`-ing unnecessarily. + +## Dependencies + +- Prefer well-maintained, widely-used crates. Pin versions in `Cargo.toml`. +- Minimize the dependency tree. Avoid adding crates for trivial functionality. +- Use `cargo audit` to check for known vulnerabilities. + +## Documentation + +- All public items (`pub`) must have doc comments. +- Include examples in doc comments for public functions. +- Use module-level documentation at the top of files. + +## Anti-patterns + +Common but counterproductive solutions. **Avoid them.** + +### Clone to satisfy the borrow checker + +Do not resolve borrow checker errors by cloning without understanding the consequences. Cloning creates independent copies — changes to one are not reflected in the other. + +If the borrow checker complains, first understand the ownership issue. Use `mem::take`, restructure borrows, or redesign the data flow. + +**Exception:** `Rc` and `Arc` are designed for shared ownership via clone. Cloning them is cheap and correct. Deliberate cloning is also fine when ownership semantics require it, or for prototypes and non-performance-critical code. + +### `#![deny(warnings)]` + +Do not use `#![deny(warnings)]` in crate roots. New compiler versions may introduce new warnings, breaking builds unexpectedly. + +Instead, deny specific named lints explicitly, or use `RUSTFLAGS="-D warnings"` in CI. This preserves Rust's stability guarantees while still enforcing lint discipline. + +### Deref polymorphism + +Do not misuse the `Deref` trait to emulate struct inheritance. `Deref` is designed for smart pointers (`pointer-to-T` → `T`), not for converting between arbitrary types. + +It does not introduce subtyping. Traits on the inner type are not automatically available on the outer type. It interacts badly with generics and bounds checking. + +Instead, use composition with explicit delegation methods, or use traits for shared behaviour. + +## Further reference + +Before implementing any non-trivial pattern — builders, visitors, newtype wrappers, RAII guards, command objects, struct decomposition for borrow splitting, etc. — read [idioms.md](references/idioms.md). It covers the community-agreed idioms and design patterns for Rust, and getting these right the first time avoids painful refactors later. diff --git a/skills/writing-rust/references/idioms.md b/skills/writing-rust/references/idioms.md new file mode 100644 index 0000000000000000000000000000000000000000..d5d5c99cc613cdc141b9380efcda242466c20ca8 --- /dev/null +++ b/skills/writing-rust/references/idioms.md @@ -0,0 +1,147 @@ +# Rust Idioms and Design Patterns + +## Contents + +- [Idioms](#idioms) + - [Borrowed types for arguments](#borrowed-types-for-arguments) + - [String concatenation with format!](#string-concatenation-with-format) + - [Constructors and Default](#constructors-and-default) + - [Collections as smart pointers](#collections-as-smart-pointers) + - [Finalisation in destructors](#finalisation-in-destructors) + - [mem::take and mem::replace](#memtake-and-memreplace) + - [On-stack dynamic dispatch](#on-stack-dynamic-dispatch) + - [Iterating over Option](#iterating-over-option) + - [Pass variables to closure](#pass-variables-to-closure) + - [non_exhaustive for extensibility](#non_exhaustive-for-extensibility) + - [Temporary mutability](#temporary-mutability) + - [Return consumed argument on error](#return-consumed-argument-on-error) +- [Behavioural patterns](#behavioural-patterns) + - [Command](#command) + - [Interpreter](#interpreter) + - [Newtype](#newtype) + - [RAII with guards](#raii-with-guards) + - [Strategy](#strategy) + - [Visitor](#visitor) +- [Creational patterns](#creational-patterns) + - [Builder](#builder) + - [Fold](#fold) +- [Structural patterns](#structural-patterns) + - [Struct decomposition for independent borrowing](#struct-decomposition-for-independent-borrowing) + - [Prefer small crates](#prefer-small-crates) + - [Contain unsafety in small modules](#contain-unsafety-in-small-modules) + - [Custom traits for complex type bounds](#custom-traits-for-complex-type-bounds) +- [Functional patterns](#functional-patterns) + +## Idioms + +### Borrowed types for arguments + +Prefer `&str` over `&String`, `&[T]` over `&Vec`, and `&T` over `&Box` in function parameters. This avoids unnecessary indirection and accepts more input types through deref coercion. + +### String concatenation with format! + +Prefer `format!("Hello {name}!")` over manual `push`/`push_str` chains for readability. For performance-critical paths where the string can be pre-allocated, manual push operations may be faster. + +### Constructors and Default + +Rust has no language-level constructors. Use an associated function called `new` to create objects. If the type has a sensible zero/empty state, implement the `Default` trait. It is common and expected to provide both `Default` and `new`, even if they are functionally identical. + +`Default` enables usage with `or_default` functions throughout the standard library and partial initialization: `MyStruct { field: value, ..Default::default() }`. + +### Collections as smart pointers + +Implement `Deref` for owning collections to provide a borrowed view (e.g., `Vec` → `&[T]`, `String` → `&str`). Implement methods on the borrowed view rather than the owning type where possible. + +### Finalisation in destructors + +Use `Drop` implementations to ensure cleanup code runs on all exit paths (early returns, `?`, panics). Assign the guard object to a named variable (not just `_`) to prevent immediate destruction. Destructors are not guaranteed to run in all cases (infinite loops, double panics). + +### mem::take and mem::replace + +When transforming an enum variant in place, use `mem::take(name)` to move values out without cloning. This avoids the "clone to satisfy the borrow checker" anti-pattern. For `Option` fields, prefer `Option::take()`. + +### On-stack dynamic dispatch + +When dynamic dispatch is needed but heap allocation is not, use `&mut dyn Trait` with temporary values. Since Rust 1.79.0, the compiler automatically extends lifetimes of temporaries in `&` or `&mut`. + +### Iterating over Option + +`Option` implements `IntoIterator`, so it works with `.extend()`, `.chain()`, and `for` loops. Use `std::iter::once` as a more readable alternative to `Some(foo).into_iter()` when the value is always present. + +### Pass variables to closure + +Use a separate scope block before the closure to prepare variables (clone, borrow, move) rather than creating separate named variables like `num2_cloned`. This groups captured state with the closure's definition. + +### non_exhaustive for extensibility + +Apply `#[non_exhaustive]` to public structs and enums that may gain fields or variants in the future, to maintain backwards compatibility across crate boundaries. Within a crate, a private field (e.g., `_b: ()`) achieves a similar effect. Use deliberately — incrementing the major version when adding fields or variants is often better. + +### Temporary mutability + +When data must be prepared mutably but then used immutably, use a nested block or variable rebinding (`let data = data;`) to enforce immutability after preparation. + +### Return consumed argument on error + +If a fallible function takes ownership of an argument, include that argument in the error type so the caller can recover it and retry. Example: `String::from_utf8` returns the original `Vec` inside `FromUtf8Error`. + +## Behavioural patterns + +### Command + +Separate actions into objects and pass them as parameters. Three approaches: trait objects (complex commands with state), function pointers (simple stateless commands), and `Fn` trait objects (closures). Use trait objects when commands need multiple functions and state; use function pointers or closures for simple, stateless cases. + +### Interpreter + +Express recurring problem instances in a domain-specific language and implement an interpreter. `macro_rules!` can serve as a lightweight compile-time interpreter for simple DSLs. + +### Newtype + +Use a tuple struct with a single field to create a distinct type (e.g., `struct Password(String)`). Provides type safety, encapsulation, and the ability to implement custom traits on existing types. Zero-cost abstraction. Consider the `derive_more` crate to reduce boilerplate pass-through impls. + +### RAII with guards + +Tie resource acquisition to object creation and release to destruction (`Drop`). Use guard objects to mediate access — the borrow checker ensures references cannot outlive the guard. Classic example: `MutexGuard`. + +### Strategy + +Define an abstract algorithm skeleton and let implementations be swapped via traits or closures. Serde is an excellent real-world example: `Serialize`/`Deserialize` traits allow swapping formats transparently. + +### Visitor + +Encapsulate an algorithm operating over a heterogeneous collection without modifying the data types. Define `visit_*` methods on a `Visitor` trait for each type; provide `walk_*` helpers to factor out traversal. The visitor can be stateful. + +## Creational patterns + +### Builder + +Construct complex objects step by step using a separate builder type. Provide a `builder()` method on the target type. Return the builder by value from each setter for method chaining: `FooBuilder::new().name("x").build()`. Consider the `derive_builder` crate. + +### Fold + +Transform a data structure by running an algorithm over each node, producing a new structure. Provide default `fold_*` methods that recurse into children, allowing implementors to override only the nodes they care about. Related to visitor, but produces a new structure. + +## Structural patterns + +### Struct decomposition for independent borrowing + +When the borrow checker prevents simultaneous borrows of different fields in a large struct, decompose into smaller structs. Compose them back; each can then be borrowed independently. Often leads to better design. + +### Prefer small crates + +Build small, focused crates that do one thing well. Easier to understand, encourage modularity, and allow parallel compilation. Be mindful of dependency quality. + +### Contain unsafety in small modules + +Isolate `unsafe` code in the smallest possible module that upholds the needed invariants. Build a safe interface on top. This restricts the audit surface. + +### Custom traits for complex type bounds + +When trait bounds become unwieldy (especially with `Fn` traits), introduce a new trait with a generic `impl` for all types satisfying the original bound. Reduces verbosity and increases expressiveness. + +## Functional patterns + +Rust supports many functional paradigms alongside its imperative core: + +- Prefer declarative iterator chains (`.fold()`, `.map()`, `.filter()`) over imperative loops when they improve clarity. +- Use generics as type classes. Rust's generic type parameters create type class constraints; different filled-in parameters create different types with potentially different `impl` blocks. +- Apply YAGNI — many traditional OO patterns are unnecessary in Rust due to traits, enums, and the type system.