Rust Flexible & Safe APIs
A flexible API accepts the loosest input that works and lets types carry meaning. Validation happens once, at the border, so changes tomorrow do not break today's callers.
Accept the loosest input that works
Taking a slice or a Vec forces the caller to build exactly that. IntoIterator accepts both — and arrays, ranges and map results too.
fn total<I: IntoIterator<Item = i32>>(numbers: I) -> i32 {
numbers.into_iter().sum()
}
total([1, 2, 3]); // an array works
total(vec![4, 5]); // and so does a VecLet the caller decide about copying
Borrowing a parameter and cloning it inside hides a cost at every call. If a function must own the value, say so in the signature.
fn keep(name: String) -> String { name } // ownership in, ownership out — no hidden cloneMeaning in types, not bools
Two bool arguments can be swapped by accident and read as noise at the call site. Small enums name each choice, and a new variant can be added later without breaking the signature.
enum Border { Shown, Hidden }
widget.set_border(Border::Shown); // clearer than widget.set_border(true)Private fields leave room to grow
A public field is a promise forever. Private fields — or #[non_exhaustive] on a public enum or struct — let you change the inside later.
pub struct Config {
retries: u32, // private: free to change later
}
#[non_exhaustive]
pub enum Mood { Calm, Busy } // outside code may not match on this aloneExample
#[derive(Debug)]
struct Port(u16);
impl Port {
fn new(value: u16) -> Option<Port> {
if value >= 1024 {
Some(Port(value))
} else {
None
}
}
}
#[derive(Debug)]
#[allow(dead_code)]
enum Mode {
Fast,
Careful,
}
fn announce(mode: Mode, port: &Port) {
println!("mode = {:?}, port = {}", mode, port.0);
}
fn total<I: IntoIterator<Item = i32>>(numbers: I) -> i32 {
numbers.into_iter().sum()
}
fn main() {
let port = Port::new(8080);
println!("port = {:?}", port);
println!("low = {:?}", Port::new(80));
if let Some(p) = port {
announce(Mode::Fast, &p);
}
println!("total = {}", total([1, 2, 3]));
println!("total = {}", total(vec![4, 5]));
}Key points
- Flexibility means loose inputs, meaningful types, and one check at the border.
- Validate in order of strength.
- First, use a type.
- A type rules bad values out.
- Next, check while processing.
- Then use a debug_assert.
- Only then offer an _unchecked sibling function.
- Experts can opt out with it.
- Hand back intermediate results when they are nearly free.
- HashMap::insert returns the value it replaced.
- That saves the caller a second lookup.
- A destructor is the Drop impl.
- It must never fail.
- It must never block.
- Offer a close() method instead.
- It returns Result for the real teardown.
- Let Drop do a quiet best effort.
- Derive Debug on every public type, even when the output is not pretty yet: {:?} in logs and assert failures is how users will meet your type when something goes wrong
- The full checklist lives at rust-lang.github.io/api-guidelines.
In this example, each design tool does one job. Port is a newtype. Its constructor checks once, at the border. 8080 is accepted and wrapped. 80 is refused with None. Every later use of a Port can trust it. announce takes a Mode enum instead of a bool, so the call site reads Mode::Fast out loud. total accepts anything IntoIterator, so the same function sums an array and a Vec. Debug is derived on both types, which is what makes the {:?} lines print Port(8080) and Fast.
Note
This lesson condenses the API Guidelines' flexibility, type-safety and dependability chapters.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
A function needs a String it will keep. Which signature is the honest one?