Idioms and design patterns
Going further · Lesson 53
An idiom is the way Rust code usually says a common thing: wrap a number in a type of its own, build a value in one place that checks it, make a wrong state impossible to write. Many are design patterns you already know, trimmed because enums, traits and ownership do part of the work. This page shows the ones you will read most, the compiler error each one turns a mistake into, and the habits that look like idioms and are not.
Coming from GoA defined type (type UserID int) is the newtype, and functional options are the cousin of the Rust builder. Go chose the zero value on purpose (var p Port is 0), and most Go types are designed so it is valid; the cost shows when it should not be, so "a Port that cannot be invalid" is a convention there. A Rust type has no zero value unless you write one, so a Port comes from the function you wrote. Cleanup is also different: defer runs at the end of the function, a Rust Drop guard at the end of its scope.
Coming from Kotlinvalue class is the newtype, default and named arguments plus copy do much of what a Rust builder or ..Default::default() does, and a sealed hierarchy is an enum. Rust has no named or default arguments, so the builder is more common than in Kotlin. use {} is the nearest to a Drop guard, but it is written by the caller at each use; a Rust guard runs without it.
Coming from PythonThe same designs exist, checked by the compiler where Python relies on convention. typing.NewType is erased at run time (the page below passes an order id to a function that wants a user id, and it runs). A class that checks in __init__ or __post_init__ is the nearest Python form of "parse, don't validate": no invalid Port gets built, but the failure is an exception, so nothing makes the caller handle it. A with block is the nearest to a Drop guard, but the caller has to write it.
Coming from TypeScriptBranded types and discriminated unions are your newtype and enum, and tsc --strict checks them. They are erased when the program runs (the page below prints typeof of a branded id: number). A Rust newtype takes no more space than its field, so it is gone at run time too; what differs is the build, where no cast turns a number into a Port.
Coming from JavaA record around one field is the newtype, a nested Builder class is the builder, and sealed interfaces with a switch are enums. A Strategy is already an interface or a lambda in Java, and a Rust trait or closure is the same thing; State has no base class to subclass per state, so it is an enum or a typestate. try-with-resources is the nearest to Drop, but you can forget to write the try.
Coming from C#A readonly record struct is the newtype, init properties with with expressions do the job of a builder and ..Default::default(), and using is the nearest to a Drop guard. A Rust type has no constructors: an associated function such as new or parse is an ordinary function that returns the value, so it can return a Result.
Coming from Ctypedef int UserId; names a type without making one: the page below passes an order id to a function that wants a user id, and it compiles under -Wall -Wextra. A one-field Rust struct is a new type. Designated initializers ({.port = 9000}) give C zero for everything you leave out, and cleanup is goto cleanup by hand, where Rust runs Drop for you.
Coming from C++RAII is where Drop comes from, an explicit one-field struct is the newtype, and std::variant is an enum. What changes is that a Rust function builds the value, so it can return a Result the caller has to handle (a C++ constructor has no return value, so it throws), and a moved-from value cannot be used.
The quick comparison
mod port { /// A port from 1 to 65535. The field is private: only `parse` can build one. pub struct Port(u16);
impl Port { pub fn parse(text: &str) -> Result<Port, String> { let n: u32 = text.parse().map_err(|_| "not a number".to_string())?; if n == 0 || n > 65535 { return Err(format!("{n} is outside 1-65535")); } Ok(Port(n as u16)) }
pub fn get(&self) -> u16 { self.0 } }}
use port::Port;
// A Port is already valid, so this function has nothing to check.fn listen(port: &Port) { println!("listening on {}", port.get());}
fn main() { for text in ["8080", "0", "http"] { match Port::parse(text) { Ok(port) => listen(&port), Err(e) => println!("{text:?}: {e}"), } }}listening on 8080 "0": 0 is outside 1-65535 "http": not a number
package main
import ( "fmt" "strconv")
// Port is valid by construction, if every caller goes through ParsePort.// In package main the field is visible to the whole package; in its own// package, `n` would be hidden from other packages.type Port struct{ n uint16 }
func ParsePort(text string) (Port, error) { n, err := strconv.ParseUint(text, 10, 32) if err != nil { return Port{}, fmt.Errorf("not a number") } if n == 0 || n > 65535 { return Port{}, fmt.Errorf("%d is outside 1-65535", n) } return Port{uint16(n)}, nil}
func listen(p Port) { fmt.Println("listening on", p.n)}
func main() { for _, text := range []string{"8080", "0", "http"} { port, err := ParsePort(text) if err != nil { fmt.Printf("%q: %v\n", text, err) continue } listen(port) }}listening on 8080 "0": 0 is outside 1-65535 "http": not a number
// The constructor is private, so Port.parse is the only way in.class Port private constructor(val number: Int) { companion object { fun parse(text: String): Port { val n = text.toLongOrNull() ?: throw IllegalArgumentException("not a number") if (n < 1 || n > 65535) throw IllegalArgumentException("$n is outside 1-65535") return Port(n.toInt()) } }}
fun listen(port: Port) { println("listening on ${port.number}")}
fun main() { for (text in listOf("8080", "0", "http")) { try { listen(Port.parse(text)) } catch (e: IllegalArgumentException) { println("\"$text\": ${e.message}") } }}listening on 8080 "0": 0 is outside 1-65535 "http": not a number
class Port: """The leading underscore is a convention: Python does not stop Port(0)."""
def __init__(self, number): self._number = number
@classmethod def parse(cls, text): try: n = int(text) except ValueError: raise ValueError("not a number") from None if n < 1 or n > 65535: raise ValueError(f"{n} is outside 1-65535") return cls(n)
@property def number(self): return self._number
def listen(port): print("listening on", port.number)
for text in ["8080", "0", "http"]: try: listen(Port.parse(text)) except ValueError as e: print(f'"{text}": {e}')listening on 8080 "0": 0 is outside 1-65535 "http": not a number
class Port { // A private constructor: outside the class, `new Port(0)` does not compile. private constructor(readonly number: number) {}
static parse(text: string): Port { if (!/^\d+$/.test(text)) throw new Error("not a number"); const n = Number(text); if (n < 1 || n > 65535) throw new Error(`${n} is outside 1-65535`); return new Port(n); }}
function listen(port: Port): void { console.log(`listening on ${port.number}`);}
for (const text of ["8080", "0", "http"]) { try { listen(Port.parse(text)); } catch (e) { console.log(`"${text}": ${(e as Error).message}`); }}listening on 8080 "0": 0 is outside 1-65535 "http": not a number
public class Main { // The constructor is private, so Port.parse is the only way in. static final class Port { private final int number;
private Port(int number) { this.number = number; }
static Port parse(String text) { long n; try { n = Long.parseLong(text); } catch (NumberFormatException e) { throw new IllegalArgumentException("not a number"); } if (n < 1 || n > 65535) { throw new IllegalArgumentException(n + " is outside 1-65535"); } return new Port((int) n); }
int number() { return number; } }
static void listen(Port port) { System.out.println("listening on " + port.number()); }
public static void main(String[] args) { for (String text : new String[] {"8080", "0", "http"}) { try { listen(Port.parse(text)); } catch (IllegalArgumentException e) { System.out.println("\"" + text + "\": " + e.getMessage()); } } }}listening on 8080 "0": 0 is outside 1-65535 "http": not a number
// The constructor is private, so Port.Parse is the only way in.class Port{ public int Number { get; }
private Port(int number) { Number = number; }
public static Port Parse(string text) { if (!long.TryParse(text, out var n)) throw new FormatException("not a number"); if (n < 1 || n > 65535) throw new FormatException($"{n} is outside 1-65535"); return new Port((int)n); }}
class Program{ static void Listen(Port port) => Console.WriteLine($"listening on {port.Number}");
static void Main() { foreach (var text in new[] { "8080", "0", "http" }) { try { Listen(Port.Parse(text)); } catch (FormatException e) { Console.WriteLine($"\"{text}\": {e.Message}"); } } }}listening on 8080 "0": 0 is outside 1-65535 "http": not a number
#include <stdio.h>#include <stdlib.h>
/* The struct is a plain value: nothing stops `struct Port p = {0};`. In a library, the header would only declare `struct Port;` so callers could hold a pointer but not build one. */struct Port { unsigned short number;};
/* Returns 0 on success; on failure returns -1 and sets *error. */static int parse_port(const char *text, struct Port *out, const char **error) { char *end; unsigned long n = strtoul(text, &end, 10); if (*text == '\0' || *end != '\0') { *error = "not a number"; return -1; } if (n < 1 || n > 65535) { static char message[64]; snprintf(message, sizeof message, "%lu is outside 1-65535", n); *error = message; return -1; } out->number = (unsigned short)n; return 0;}
static void listen_on(const struct Port *port) { printf("listening on %d\n", port->number);}
int main(void) { const char *inputs[] = {"8080", "0", "http"}; for (int i = 0; i < 3; i++) { struct Port port; const char *error; if (parse_port(inputs[i], &port, &error) == 0) { listen_on(&port); } else { printf("\"%s\": %s\n", inputs[i], error); } } return 0;}listening on 8080 "0": 0 is outside 1-65535 "http": not a number
#include <iostream>#include <stdexcept>#include <string>
// The constructor is private, so Port::parse is the only way in.class Port {public: static Port parse(const std::string& text) { std::size_t used = 0; unsigned long n = 0; try { n = std::stoul(text, &used); } catch (const std::exception&) { throw std::invalid_argument("not a number"); } if (used != text.size()) throw std::invalid_argument("not a number"); if (n < 1 || n > 65535) throw std::invalid_argument(std::to_string(n) + " is outside 1-65535"); return Port(static_cast<unsigned short>(n)); }
unsigned short number() const { return number_; }
private: explicit Port(unsigned short number) : number_(number) {} unsigned short number_;};
void listen_on(const Port& port) { std::cout << "listening on " << port.number() << "\n";}
int main() { for (const std::string text : {"8080", "0", "http"}) { try { listen_on(Port::parse(text)); } catch (const std::invalid_argument& e) { std::cout << '"' << text << "\": " << e.what() << "\n"; } }}listening on 8080 "0": 0 is outside 1-65535 "http": not a number
What's different
- A newtype is a struct with one field. It is a different type from the field's type, so the compiler refuses to mix them up, and its field can be private so that your own functions build it.
- "Parse, don't validate": turn loosely typed input into a type that cannot be invalid, once, at the edge; everything after takes the strong type.
- Enums with data make illegal states unrepresentable: a download that is both done and failed has no spelling. A typestate goes further and puts the state in the type, so calling
sendbeforeconnectis a compile error. Defaultwith..Default::default()and a builder stand in for the named and default arguments that Rust does not have. A guard type with aDropimpl ties cleanup to a scope.- Some habits look like idioms and are not: a
.clone()that silences the borrow checker,unwrap()in a library,Rc<RefCell<_>>for everything, text where an enum belongs, generics nobody uses, andBox<dyn Error>as a library's error type. Each has a before and an after below.
Copying a struct with .. and then using the original
Looks fine: in Go, Kotlin, Python, TypeScript, Java, C#, C and C++ it runs fine. Rust won't compile it.
Works in Go. In Rust it's just a rule.Works in Kotlin. In Rust it's just a rule.Works in Python. In Rust it's just a rule.Works in TypeScript. In Rust it's just a rule.Works in Java. In Rust it's just a rule.Works in C#. In Rust it's just a rule.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.#[derive(Debug, Clone)]struct Config { name: String, port: u16, verbose: bool,}
fn main() { let base = Config { name: "service".to_string(), port: 8080, verbose: false, }; // "The same, but on another port." let test = Config { port: 9000, ..base }; println!("{base:?}"); println!("{test:?}");}package main
import "fmt"
type Config struct { Name string Port uint16 Verbose bool}
func main() { base := Config{Name: "service", Port: 8080, Verbose: false} test := base // a struct assignment copies the fields test.Port = 9000 fmt.Printf("%+v\n", base) fmt.Printf("%+v\n", test)}{Name:service Port:8080 Verbose:false}
{Name:service Port:9000 Verbose:false}data class Config(val name: String, val port: Int, val verbose: Boolean)
fun main() { val base = Config("service", 8080, false) val test = base.copy(port = 9000) println(base) println(test)}Config(name=service, port=8080, verbose=false) Config(name=service, port=9000, verbose=false)
from dataclasses import dataclass, replace
@dataclassclass Config: name: str port: int verbose: bool
base = Config("service", 8080, False)test = replace(base, port=9000)print(base)print(test)Config(name='service', port=8080, verbose=False) Config(name='service', port=9000, verbose=False)
interface Config { name: string; port: number; verbose: boolean;}
const base: Config = { name: "service", port: 8080, verbose: false };const test: Config = { ...base, port: 9000 };console.log(base);console.log(test);{ name: 'service', port: 8080, verbose: false }
{ name: 'service', port: 9000, verbose: false }public class Main { record Config(String name, int port, boolean verbose) {}
public static void main(String[] args) { Config base = new Config("service", 8080, false); // A record has no "copy with"; you pass the fields you keep. Config test = new Config(base.name(), 9000, base.verbose()); System.out.println(base); System.out.println(test); }}Config[name=service, port=8080, verbose=false] Config[name=service, port=9000, verbose=false]
var baseConfig = new Config("service", 8080, false);var test = baseConfig with { Port = 9000 };Console.WriteLine(baseConfig);Console.WriteLine(test);
record Config(string Name, int Port, bool Verbose);Config { Name = service, Port = 8080, Verbose = False }
Config { Name = service, Port = 9000, Verbose = False }#include <stdio.h>
struct Config { const char *name; unsigned short port; int verbose;};
int main(void) { struct Config base = {"service", 8080, 0}; struct Config test = base; /* copies the fields, including the name pointer */ test.port = 9000; printf("%s %d %d\n", base.name, base.port, base.verbose); printf("%s %d %d\n", test.name, test.port, test.verbose); return 0;}service 8080 0 service 9000 0
#include <iostream>#include <string>
struct Config { std::string name; unsigned short port; bool verbose;};
int main() { Config base{"service", 8080, false}; Config test = base; // copies every field, the string too test.port = 9000; std::cout << base.name << " " << base.port << " " << base.verbose << "\n"; std::cout << test.name << " " << test.port << " " << test.verbose << "\n";}service 8080 0 service 9000 0
What does the compiler say? Guess first, then open.
error[E0382]: borrow of partially moved value: `base` --> src/main.rs:16:16 | 15 | let test = Config { port: 9000, ..base }; | ----------------------------- value partially moved here 16 | println!("{base:?}"); | ^^^^ value borrowed here after partial move | = note: partial move occurs because `base.name` has type `String`, which does not implement the `Copy` trait
Go runs this and prints both structs. test := base copies the three fields. The string is shared, which is fine because a Go string cannot change.
Kotlin runs this and prints both. copy(port = 9000) makes a new Config; the String is immutable, so the two share it.
Python runs this and prints both. dataclasses.replace builds a new Config; the str is immutable, so the two share it.
TypeScript runs this and prints both. The spread copies the properties one level deep; the string is a primitive. An object-valued property would be shared by both.
Java runs this and prints both. A record has no copy-with, so you pass the fields you keep by hand; the String is immutable and shared.
C# runs this and prints both. with copies the record and changes Port; the string is immutable and shared.
C runs this and prints both. The struct assignment copies the fields, and here name is a pointer to a string literal that both copies share. With a malloced name, freeing both copies would free it twice (not run).
C++ runs this and prints both. The copy duplicates the std::string too, so both are whole; the allocation is the cost Rust asks you to write as .clone().
..base takes the fields you did not name: name and verbose. verbose is Copy, so it is copied, but name is a String, so it is moved into test. base is now partly moved, and printing all of it is an error.
A String has one owner, and a copy that allocates is something Rust makes you ask for with .clone(). So ..base moves what it cannot copy, and base stays whole when the fields taken were all Copy (name the String yourself and it works; Structs shows the same rule). The fix is ..base.clone() when you want a full copy, or ..Default::default() when you only wanted defaults.
The fix
Compiles and runs.
#[derive(Debug, Clone)]struct Config { name: String, port: u16, verbose: bool,}
fn main() { let base = Config { name: "service".to_string(), port: 8080, verbose: false, }; // `..base.clone()` hands over a copy, so `base` stays whole. let test = Config { port: 9000, ..base.clone() }; println!("{base:?}"); println!("{test:?}"); println!( "{} on port {}, verbose: {}", test.name, test.port, test.verbose );}Config { name: "service", port: 8080, verbose: false }
Config { name: "service", port: 9000, verbose: false }
service on port 9000, verbose: falseThe toolkit
Most of these idioms are not language features: each is ordinary structs, enums, traits and Drop, arranged in a shape that Rust code tends to share, usually to move a rule out of your head and into the compiler. The rest (let ... else, matches!, impl Trait) are small features that shorten them. This page names the shapes and says what each costs. Outputs come from Linux on 64-bit ARM with Rust 1.99.1, a debug build.
If you know the design-pattern names, start here. “Close” is the word: each Rust form does the job, not the whole class diagram.
| Pattern | In Rust | Where |
|---|---|---|
| Builder | a builder type, or Default with .. |
Building a value |
| Strategy | a closure or trait: impl Trait when the caller picks, Box<dyn Fn> or Box<dyn Trait> when chosen at run time |
Traits at the edges |
| State | an enum with match, or typestate |
Illegal states, Typestate |
| Adapter, wrapper | a newtype that implements the trait you need | Newtype |
| Static factory method | an associated function: new, parse, try_from |
Parse, don’t validate |
Resource guard (try-with-resources, using, with) |
a Drop type |
Guards |
| Singleton, global instance | a static OnceLock or LazyLock (Shared state), or a value you pass in |
Observer, Visitor and Decorator have no row: they are written with traits, closures and enums, as in the Strategy row.
Newtype: a type for each meaning
A newtype is a struct with one field (Structs shows the compiler refusing a mix-up of two). Wrapping adds no data, and it lets you implement a trait on a type you do not own:
use std::fmt;use std::mem::size_of;
struct UserId(u32);struct OrderId(u32);
fn load_user(id: &UserId) -> String { format!("user #{}", id.0)}
/// A wrapper around a foreign type, so that this crate may implement a foreign trait on it.struct Names(Vec<String>);
impl fmt::Display for Names { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0.join(", ")) }}
fn main() { let user = UserId(7); let order = OrderId(7); println!("{}", load_user(&user)); println!("order #{}", order.0);
println!("size_of::<u32>(): {}", size_of::<u32>()); println!("size_of::<UserId>(): {}", size_of::<UserId>()); println!("size_of::<Names>(): {}", size_of::<Names>()); println!("size_of::<Vec<String>>(): {}", size_of::<Vec<String>>());
println!("{}", Names(vec!["Ada".to_string(), "Grace".to_string()]));}user #7 order #7 size_of::<u32>(): 4 size_of::<UserId>(): 4 size_of::<Names>(): 24 size_of::<Vec<String>>(): 24 Ada, Grace
The sizes match their fields. Names is the second use: the orphan rule lets you implement a trait only if you own the trait or the type, so impl Display for Vec<String> is refused (the Book uses a wrapper). The wrapper starts with none of the inner type’s methods or operators. A private field turns a newtype from a label into a guarantee, as the next section shows.
Parse, don’t validate
A function that returns bool leaves every later function to trust that someone called it. A function that returns a Port hands out proof: the check happens once, where text becomes a value, and listen takes a Port and checks nothing. The phrase is from an essay by Alexis King:
use std::str::FromStr;
mod port { use std::fmt; use std::str::FromStr;
/// A TCP port from 1 to 65535. The only way to get one is `Port::try_from` or `parse`. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Port(u16);
#[derive(Debug, PartialEq)] pub enum PortError { NotANumber, OutOfRange(u32), }
impl fmt::Display for PortError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { PortError::NotANumber => write!(f, "not a number"), PortError::OutOfRange(n) => write!(f, "{n} is outside 1-65535"), } } }
impl TryFrom<u32> for Port { type Error = PortError;
fn try_from(n: u32) -> Result<Port, PortError> { match u16::try_from(n) { Ok(p) if p != 0 => Ok(Port(p)), _ => Err(PortError::OutOfRange(n)), } } }
impl FromStr for Port { type Err = PortError;
fn from_str(s: &str) -> Result<Port, PortError> { let n: u32 = s.trim().parse().map_err(|_| PortError::NotANumber)?; Port::try_from(n) } }
impl Port { pub fn get(self) -> u16 { self.0 } }}
use port::Port;
/// No check here: a `Port` is already known to be valid.fn listen(port: Port) -> String { format!("listening on {}", port.get())}
fn main() { for text in ["8080", "0", "70000", "http"] { match Port::from_str(text) { Ok(port) => println!("{text:>6} -> {}", listen(port)), Err(e) => println!("{text:>6} -> rejected: {e}"), } }} 8080 -> listening on 8080
0 -> rejected: 0 is outside 1-65535
70000 -> rejected: 70000 is outside 1-65535
http -> rejected: not a numberTwo things make it work. The field is private to the module, so code outside it cannot write Port(0):
mod port { pub struct Port(u16);
impl Port { pub fn get(&self) -> u16 { self.0 } }}
fn main() { let p = port::Port(0); println!("{}", p.get());}error[E0603]: tuple struct constructor `Port` is private --> src/main.rs:12:19 | 2 | pub struct Port(u16); | --- a constructor is private if any of the fields is private ... 12 | let p = port::Port(0); | ^^^^ private tuple struct constructor | note: the tuple struct constructor `Port` is defined here --> src/main.rs:2:5 | 2 | pub struct Port(u16); | ^^^^^^^^^^^^^^^^^^^^^ help: consider making the field publicly accessible | 2 | pub struct Port(pub u16); | +++
And the constructor returns a Result, so a caller has to deal with the bad input. impl TryFrom<u32> for Port is the standard name for “convert, and it may fail” (the first exercise writes one), FromStr is what str::parse calls, and From is for conversions that cannot fail (Common traits). Code inside the port module can still build a bad Port, because privacy is per module (Modules): the module is the boundary you have to trust.
Make illegal states unrepresentable
Three fields that must agree, and nothing making them agree:
/// Three fields that must agree, and nothing makes them.#[derive(Debug)]struct Download { done: bool, error: Option<String>, bytes: Option<u64>,}
fn describe(d: &Download) -> String { if d.done { format!("finished, {} bytes", d.bytes.unwrap_or(0)) } else if let Some(e) = &d.error { format!("failed: {e}") } else { "running".to_string() }}
fn main() { let fine = Download { done: true, error: None, bytes: Some(512), }; // Compiles. Is it finished or failed? `describe` quietly picks one. let contradiction = Download { done: true, error: Some("timeout".to_string()), bytes: None, }; println!("{}", describe(&fine)); println!("{}", describe(&contradiction));}finished, 512 bytes finished, 0 bytes
contradiction is done and failed at once, and describe quietly picks “finished”. An enum has one variant per state, and each carries only the data that state has (Enums):
/// One variant per state; each carries only the data that state has.#[derive(Debug)]enum Download { Running, Done { bytes: u64 }, Failed { reason: String },}
fn describe(d: &Download) -> String { match d { Download::Running => "running".to_string(), Download::Done { bytes } => format!("finished, {bytes} bytes"), Download::Failed { reason } => format!("failed: {reason}"), }}
fn main() { let states = [ Download::Running, Download::Done { bytes: 512 }, Download::Failed { reason: "timeout".to_string(), }, ]; for s in &states { println!("{}", describe(s)); } // "done and failed at once" has no spelling: there is no variant that holds both.}running finished, 512 bytes failed: timeout
There is no spelling for “done and failed”. When you add a state, a match that forgets it and has no _ arm stops compiling (E0004, shown in Match), so the compiler lists the places to update. Java’s sealed types and Kotlin’s when check this too (Enums compares them).
Typestate: the state is in the type
An enum is right when the next state depends on data you learn while running. When the order of calls is known in advance, you can put the state in the type, so a call in the wrong state does not compile. Closed and Open are unit structs, types with no fields. Rust rejects a type parameter that no field uses (E0392, compile-checked), so PhantomData<State> is the field that mentions State and holds no data. connect takes self by value, so the closed connection is gone and an open one comes back; every transition rebuilds the struct:
use std::marker::PhantomData;use std::mem::size_of;
struct Closed;struct Open;
struct Connection<State> { host: String, sent: u32, state: PhantomData<State>,}
impl Connection<Closed> { fn new(host: &str) -> Connection<Closed> { Connection { host: host.to_string(), sent: 0, state: PhantomData, } }
/// Takes `self` by value: the closed connection is gone, an open one comes back. fn connect(self) -> Connection<Open> { println!("connecting to {}", self.host); Connection { host: self.host, sent: self.sent, state: PhantomData, } }}
impl Connection<Open> { fn send(&mut self, text: &str) { self.sent += 1; println!("sent {text:?} (message {})", self.sent); }
fn close(self) -> Connection<Closed> { println!("closing after {} messages", self.sent); Connection { host: self.host, sent: self.sent, state: PhantomData, } }}
fn main() { let conn = Connection::new("example.org"); let mut conn = conn.connect(); conn.send("hello"); conn.send("world"); let conn = conn.close();
println!( "size_of::<Connection<Closed>>(): {}", size_of::<Connection<Closed>>() ); println!( "size_of::<Connection<Open>>(): {}", size_of::<Connection<Open>>() ); println!( "size_of::<(String, u32)>(): {}", size_of::<(String, u32)>() ); println!("host is still {}", conn.host);}connecting to example.org sent "hello" (message 1) sent "world" (message 2) closing after 2 messages size_of::<Connection<Closed>>(): 32 size_of::<Connection<Open>>(): 32 size_of::<(String, u32)>(): 32 host is still example.org
All three sizes match: the state costs no space in this program. Misuse is an error about a method that does not exist yet:
use std::marker::PhantomData;
struct Closed;struct Open;
struct Connection<State> { state: PhantomData<State>,}
impl Connection<Closed> { fn new() -> Connection<Closed> { Connection { state: PhantomData } }
fn connect(self) -> Connection<Open> { Connection { state: PhantomData } }}
impl Connection<Open> { fn send(&mut self, text: &str) { println!("sent {text:?}"); }}
fn main() { let mut conn = Connection::new(); conn.send("hello"); let _open = conn.connect();}error[E0599]: no method named `send` found for struct `Connection<Closed>` in the current scope --> src/main.rs:28:10 | 6 | struct Connection<State> { | ------------------------ method `send` not found for this struct ... 28 | conn.send("hello"); | ^^^^ method not found in `Connection<Closed>` | = note: the method was found for `Connection<Open>`
And because connect consumed the closed connection, using it again is a moved-value error:
use std::marker::PhantomData;
struct Closed;struct Open;
struct Connection<State> { state: PhantomData<State>,}
impl Connection<Closed> { fn new() -> Connection<Closed> { Connection { state: PhantomData } }
fn connect(self) -> Connection<Open> { Connection { state: PhantomData } }}
fn main() { let closed = Connection::new(); let _open = closed.connect(); let _again = closed.connect();}error[E0382]: use of moved value: `closed` --> src/main.rs:23:18 | 21 | let closed = Connection::new(); | ------ move occurs because `closed` has type `Connection<Closed>`, which does not implement the `Copy` trait 22 | let _open = closed.connect(); | --------- `closed` moved due to this method call 23 | let _again = closed.connect(); | ^^^^^^ value used here after move | note: `Connection::<Closed>::connect` takes ownership of the receiver `self`, which moves `closed` --> src/main.rs:15:16 | 15 | fn connect(self) -> Connection<Open> { | ^^^^
The price is more code, and a state read from a file or a socket needs an enum around the typed versions. The Rust Book builds the same design as an alternative to the object-oriented State pattern; Cliff Biffle’s essay is a longer tour.
Building a value: Default, update syntax and the builder
Rust has no default or named arguments (What Rust lacks). For a struct with a few public fields, Default and the update syntax .. are enough:
#[derive(Debug, Clone, PartialEq)]struct Config { host: String, port: u16, retries: u32, verbose: bool,}
impl Default for Config { fn default() -> Config { Config { host: "localhost".to_string(), port: 8080, retries: 3, verbose: false, } }}
fn main() { let plain = Config::default(); let custom = Config { port: 9000, verbose: true, ..Default::default() }; println!("{plain:?}"); println!("{custom:?}"); assert_eq!(custom.host, plain.host);}Config { host: "localhost", port: 8080, retries: 3, verbose: false }
Config { host: "localhost", port: 9000, retries: 3, verbose: true }.. takes the fields you do not name: a Copy field is copied and a String is moved, which is the gotcha above and the rule Structs shows. When only Copy fields are taken, the original stays whole (compile-checked). When fields need checking together, or the list is long, use a builder (Methods shows both shapes of setter). What this page adds is the end of the chain: one build that checks the whole value and returns a Result.
use std::time::Duration;
struct Request { url: String, method: String, retries: u32, timeout: Duration,}
#[derive(Debug, PartialEq)]enum BuildError { MissingUrl,}
struct RequestBuilder { url: Option<String>, method: String, retries: u32, timeout: Duration,}
impl RequestBuilder { fn new() -> RequestBuilder { RequestBuilder { url: None, method: "GET".to_string(), retries: 3, timeout: Duration::from_secs(30), } }
// Each setter takes `self` and returns it, so calls chain and nothing is borrowed. fn url(mut self, url: &str) -> RequestBuilder { self.url = Some(url.to_string()); self }
fn method(mut self, method: &str) -> RequestBuilder { self.method = method.to_string(); self }
fn retries(mut self, retries: u32) -> RequestBuilder { self.retries = retries; self }
/// The one place that checks the whole value. fn build(self) -> Result<Request, BuildError> { let url = self.url.ok_or(BuildError::MissingUrl)?; Ok(Request { url, method: self.method, retries: self.retries, timeout: self.timeout, }) }}
fn main() { let ok = RequestBuilder::new() .url("https://example.org") .method("POST") .retries(0) .build(); match &ok { Ok(r) => println!( "{} {} retries={} timeout={:?}", r.method, r.url, r.retries, r.timeout ), Err(e) => println!("error: {e:?}"), } let missing = RequestBuilder::new().retries(5).build(); println!("{:?}", missing.map(|r| r.url));}POST https://example.org retries=0 timeout=30s Err(MissingUrl)
The Rust Design Patterns book says the builder is common in Rust “because Rust lacks overloading and default values for function parameters”. For two or three fields, ..Default::default() is shorter.
Guards: cleanup tied to a scope
A guard is a value whose drop undoes something. drop runs when the value leaves its scope, so the cleanup happens on return and on ? as well as at the end (the MutexGuard in Shared state is one, and Smart pointers has the pointers that free on drop). You will rarely write your own: a File or a MutexGuard is usually the guard:
use std::cell::Cell;
/// Raises a counter while it is alive. The matching decrement is in `drop`, so it/// cannot be forgotten and runs on every way out of the scope.struct Depth<'a> { level: &'a Cell<u32>,}
impl<'a> Depth<'a> { fn enter(level: &'a Cell<u32>, name: &str) -> Depth<'a> { level.set(level.get() + 1); println!("{}enter {name}", " ".repeat(level.get() as usize - 1)); Depth { level } }}
impl Drop for Depth<'_> { fn drop(&mut self) { self.level.set(self.level.get() - 1); println!("{}leave", " ".repeat(self.level.get() as usize)); }}
fn parse(level: &Cell<u32>, text: &str) -> Result<u32, std::num::ParseIntError> { let _guard = Depth::enter(level, "parse"); let n = text.parse::<u32>()?; // an early return: the guard still drops Ok(n * 2)}
fn main() { let level = Cell::new(0); { let _outer = Depth::enter(&level, "main"); println!("{:?}", parse(&level, "21")); println!("{:?}", parse(&level, "x")); } println!("level after: {}", level.get());}enter main
enter parse
leave
Ok(42)
enter parse
leave
Err(ParseIntError { kind: InvalidDigit })
leave
level after: 0The second parse returned early with ?, and its leave still printed. A guard cannot be forgotten, where try-with-resources, using and with run only if the caller writes them. The exercise below also checks a panic that unwinds (the stack is cleaned up as the panic travels). Common traits compares Drop with defer, try-with-resources, using, with and C++ destructors.
Flat code
Option and Result methods, ?, let ... else, matches! and iterator chains keep code flat; each is taught with its type: Option, Errors, If/else for let ... else, Match and Iterators. A rule of thumb: ? when failure should leave the function, a combinator when a value is only being transformed, match when the branches do different things. The else block of let ... else must not fall through: it ends in return, break, continue or panic!.
Traits at the edges
impl Trait in an argument is shorthand for a generic parameter, so the caller picks the type at compile time. With a closure it is the Strategy pattern without a class per strategy (Closures). To choose the strategy at run time, store it as a Box<dyn Fn(i32) -> bool>, or a Box<dyn Trait> (Traits), as rule does:
use std::fmt::Display;
/// The rule is a parameter. Any closure (or function) with this shape fits.fn count_matching(items: &[i32], keep: impl Fn(i32) -> bool) -> usize { let mut n = 0; for &item in items { if keep(item) { n += 1; } } n}
fn is_even(n: i32) -> bool { n % 2 == 0}
/// Chosen at run time (say, from a config file): a boxed closure, one type for every rule.fn rule(name: &str) -> Option<Box<dyn Fn(i32) -> bool>> { match name { "even" => Some(Box::new(is_even)), "big" => Some(Box::new(|n| n > 5)), _ => None, }}
/// `impl Trait` for the item type too: anything that can be iterated and shown.fn show_all(items: impl IntoIterator<Item = impl Display>) -> String { let parts: Vec<String> = items.into_iter().map(|i| i.to_string()).collect(); parts.join(" | ")}
fn main() { let data = [1, 2, 3, 4, 5, 6, 7, 8]; let limit = 5; println!("even: {}", count_matching(&data, is_even)); println!("above {limit}: {}", count_matching(&data, |n| n > limit)); println!( "odd or > 6: {}", count_matching(&data, |n| n % 2 == 1 || n > 6) );
for name in ["big", "even", "prime"] { match rule(name) { Some(keep) => println!("rule {name}: {}", count_matching(&data, keep)), None => println!("rule {name}: unknown"), } }
println!("{}", show_all(vec![1, 2, 3])); println!("{}", show_all(["a", "b"])); println!("{}", show_all(data.iter().take(2)));}even: 4 above 5: 3 odd or > 6: 5 rule big: 3 rule even: 4 rule prime: unknown 1 | 2 | 3 a | b 1 | 2
An extension trait adds methods to a type you do not own, by implementing your own trait for it. The methods exist only where the trait is imported:
mod text_ext { /// Our own trait, so we may implement it for the foreign type `str`. pub trait StrExt { fn is_blank(&self) -> bool; fn first_word(&self) -> &str; }
impl StrExt for str { fn is_blank(&self) -> bool { self.trim().is_empty() }
fn first_word(&self) -> &str { self.split_whitespace().next().unwrap_or("") } }}
// The methods exist only where the trait is in scope.use text_ext::StrExt;
fn main() { println!("{}", " ".is_blank()); println!("{}", " hello world".first_word()); let owned = String::from("rust language"); println!("{}", owned.first_word()); // a String derefs to str}true hello rust
mod text_ext { pub trait StrExt { fn is_blank(&self) -> bool; }
impl StrExt for str { fn is_blank(&self) -> bool { self.trim().is_empty() } }}
fn main() { println!("{}", " ".is_blank());}error[E0599]: no method named `is_blank` found for reference `&'static str` in the current scope --> src/main.rs:14:26 | 14 | println!("{}", " ".is_blank()); | ^^^^^^^^ method not found in `&'static str` | = help: items from traits can only be used if the trait is in scope help: trait `StrExt` which provides `is_blank` is implemented but not in scope; perhaps you want to import it | 1 + use crate::text_ext::StrExt;
A sealed trait is public, but outsiders cannot implement it, because it has a supertrait in a private module that they cannot name. The API Guidelines say it lets you add methods without breaking users. Here only the shapes module can implement Shape; a library would put the private module at its root. An impl from outside fails, and the error says why:
mod shapes { mod private { pub trait Sealed {} }
pub trait Shape: private::Sealed { fn sides(&self) -> u32; }}
struct Circle;
impl shapes::Shape for Circle { fn sides(&self) -> u32 { 0 }}
fn main() { let _circle = Circle;}error[E0277]: the trait bound `Circle: Sealed` is not satisfied --> src/main.rs:13:24 | 13 | impl shapes::Shape for Circle { | ^^^^^^ unsatisfied trait bound | help: the trait `Sealed` is not implemented for `Circle` --> src/main.rs:11:1 | 11 | struct Circle; | ^^^^^^^^^^^^^ help: this trait has no implementations, consider adding one --> src/main.rs:3:9 | 3 | pub trait Sealed {} | ^^^^^^^^^^^^^^^^ note: required by a bound in `Shape` --> src/main.rs:6:22 | 6 | pub trait Shape: private::Sealed { | ^^^^^^^^^^^^^^^ required by this bound in `Shape` = note: `Shape` is a "sealed trait", because to implement it you also need to implement `shapes::private::Sealed`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it
Cow<str> holds either a borrowed or an owned string, so a function can return its input untouched when nothing changed and build a new String only when it must (Strings lists it).
Habits that look like idioms
Rust Design Patterns has a chapter of anti-patterns. These six look like help; each pair below is a real before and after.
.clone() to quiet the borrow checker
The error says team.bonus() borrows all of team while team.scores is borrowed for the loop:
struct Team { scores: Vec<u32>, bonus_per_member: u32,}
impl Team { fn bonus(&self) -> u32 { self.bonus_per_member + self.scores.len() as u32 }}
fn award(team: &mut Team) { for score in team.scores.iter_mut() { *score += team.bonus(); }}
fn main() { let mut team = Team { scores: vec![10, 20, 30], bonus_per_member: 1, }; award(&mut team); println!("{:?}", team.scores);}error[E0502]: cannot borrow `*team` as immutable because it is also borrowed as mutable --> src/main.rs:14:19 | 13 | for score in team.scores.iter_mut() { | ---------------------- | | | mutable borrow occurs here | mutable borrow later used here 14 | *score += team.bonus(); | ^^^^ immutable borrow occurs here
Cloning the list makes the error go away and the program wrong, because the bonus lands on a copy that is then thrown away:
struct Team { scores: Vec<u32>, bonus_per_member: u32,}
impl Team { fn bonus(&self) -> u32 { self.bonus_per_member + self.scores.len() as u32 }}
fn award(team: &mut Team) { // "Fixes" the borrow error by working on a copy. The copy is thrown away. let mut scores = team.scores.clone(); for score in scores.iter_mut() { *score += team.bonus(); }}
fn main() { let mut team = Team { scores: vec![10, 20, 30], bonus_per_member: 1, }; award(&mut team); println!("{:?}", team.scores);}[10, 20, 30]
The book says the same: “Any changes between the two are not synchronized” (Clone to satisfy the borrow checker). The fix is to read the one number first (Working with the borrow checker):
struct Team { scores: Vec<u32>, bonus_per_member: u32,}
impl Team { fn bonus(&self) -> u32 { self.bonus_per_member + self.scores.len() as u32 }}
fn award(team: &mut Team) { // Read the one number first (a `u32` is copied out, so no borrow is left), then change the list. let bonus = team.bonus(); for score in team.scores.iter_mut() { *score += bonus; }}
fn main() { let mut team = Team { scores: vec![10, 20, 30], bonus_per_member: 1, }; award(&mut team); println!("{:?}", team.scores);}[14, 24, 34]
A clone is sometimes right, but it is a decision about two owners, not a way to end an error.
unwrap in a library
A library that unwraps decides for every caller that bad input is fatal, and the message points into the library:
/// A "library" function: it decides, for every caller, that bad input is fatal.pub fn parse_port(text: &str) -> u16 { text.trim().parse().unwrap()}
fn main() { println!("{}", parse_port("8080")); println!("{}", parse_port("http"));}8080
thread 'main' (1) panicked at src/main.rs:3:25:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceReturn the Result, and the caller decides (Errors says when expect is right):
use std::num::ParseIntError;
/// The caller gets the failure as a value and decides what it means.pub fn parse_port(text: &str) -> Result<u16, ParseIntError> { text.trim().parse()}
fn main() { for text in ["8080", "http"] { match parse_port(text) { Ok(port) => println!("{text}: port {port}"), Err(e) => println!("{text}: {e}, using 80"), } }}8080: port 8080 http: invalid digit found in string, using 80
Rc<RefCell<_>> for everything
It compiles, and the borrow rules become run-time panics:
use std::cell::RefCell;use std::rc::Rc;
type Log = Rc<RefCell<Vec<String>>>;
fn note(log: &Log, text: String) { log.borrow_mut().push(text);}
/// Adds a follow-up note for every note so far.fn add_follow_ups(log: &Log) { for line in log.borrow().iter() { note(log, format!("follow up on {line}")); }}
fn main() { let log: Log = Rc::new(RefCell::new(vec!["call Ada".to_string()])); add_follow_ups(&log); println!("{:?}", log.borrow());}thread 'main' (1) panicked at src/main.rs:7:9: RefCell already borrowed note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
If you come from a garbage-collected language, this is what shared mutable references everywhere look like in Rust. Pass &mut down and let the compiler check the same rule. A loop that passes a plain &mut Vec to a function while iterating it is E0502 (compile-checked), and building the new notes first fixes it. Rc<RefCell<_>> is for the cases Smart pointers lists:
fn add_follow_ups(log: &mut Vec<String>) { // Build the new notes first, then add them: the list is never read and changed at once. let follow_ups: Vec<String> = log .iter() .map(|line| format!("follow up on {line}")) .collect(); log.extend(follow_ups);}
fn main() { let mut log = vec!["call Ada".to_string()]; add_follow_ups(&mut log); println!("{log:?}");}["call Ada", "follow up on call Ada"]
Text where an enum belongs
Any string compiles, and a typo falls into the default arm:
/// Stringly typed: any text compiles, and a typo falls through to the default arm.fn open_flags(mode: &str) -> &'static str { match mode { "read" => "O_RDONLY", "write" => "O_WRONLY", _ => "O_RDONLY", }}
fn main() { println!("read -> {}", open_flags("read")); println!("write -> {}", open_flags("write")); println!("wrtie -> {}", open_flags("wrtie"));}read -> O_RDONLY write -> O_WRONLY wrtie -> O_RDONLY
Parse the text into an enum once, at the edge. The typo becomes an error you can show, and inside the program a typo like Mode::Wrtie does not compile (E0599, compile-checked):
use std::str::FromStr;
#[derive(Debug, Clone, Copy)]enum Mode { Read, Write,}
impl FromStr for Mode { type Err = String;
/// Text is turned into a `Mode` once, at the edge of the program. fn from_str(s: &str) -> Result<Mode, String> { match s { "read" => Ok(Mode::Read), "write" => Ok(Mode::Write), other => Err(format!("unknown mode {other:?}")), } }}
fn open_flags(mode: Mode) -> &'static str { match mode { Mode::Read => "O_RDONLY", Mode::Write => "O_WRONLY", }}
fn main() { for text in ["read", "write", "wrtie"] { match text.parse::<Mode>() { Ok(mode) => println!("{text} -> {}", open_flags(mode)), Err(e) => println!("{text} -> {e}"), } } println!("{}", open_flags(Mode::Write));}read -> O_RDONLY write -> O_WRONLY wrtie -> unknown mode "wrtie" O_WRONLY
Generics nobody uses
A parameter nobody varies buys a longer signature, and an error when an argument does not say what it is:
/// Generic over the element type "in case" a second caller shows up.fn first_or_zero<T: Copy + Into<u32>>(items: &[T]) -> u32 { items.first().map_or(0, |&x| x.into())}
fn main() { println!("{}", first_or_zero(&[7u32, 8])); println!("{}", first_or_zero(&[]));}error[E0283]: type annotations needed --> src/main.rs:8:20 | 8 | println!("{}", first_or_zero(&[])); | ^^^^^^^^^^^^^ --- type must be known at this point | | | cannot infer type of the type parameter `T` declared on the function `first_or_zero` | = note: the type must implement `Copy` = note: the type must also implement `Into<u32>` note: required by a bound in `first_or_zero` --> src/main.rs:2:21 | 2 | fn first_or_zero<T: Copy + Into<u32>>(items: &[T]) -> u32 { | ^^^^ required by this bound in `first_or_zero` note: required by a bound in `first_or_zero` --> src/main.rs:2:28 | 2 | fn first_or_zero<T: Copy + Into<u32>>(items: &[T]) -> u32 { | ^^^^^^^^^ required by this bound in `first_or_zero` help: consider specifying a concrete type for the type parameter `T` | 8 | println!("{}", first_or_zero::</* Type */>(&[])); | ++++++++++++++
With one caller, a concrete type is clearer, and &[u32] already takes a Vec, an array and an empty slice:
/// One caller today, so one concrete signature. `&[u32]` accepts a Vec, an array and `&[]`.fn first_or_zero(items: &[u32]) -> u32 { items.first().copied().unwrap_or(0)}
fn main() { println!("{}", first_or_zero(&[7, 8])); println!("{}", first_or_zero(&vec![9])); println!("{}", first_or_zero(&[]));}7 9 0
The API Guidelines ask for generics where they let callers pass more, such as I: IntoIterator<Item = i64>. The anti-pattern is generality with no second caller (Generics).
Box<dyn Error> in a library’s API
The signature says only “some error”, so a caller that wants to react to one failure must guess its concrete type:
use std::error::Error;use std::num::ParseIntError;
/// A library function whose signature says only "some error".pub fn read_port(text: &str) -> Result<u16, Box<dyn Error>> { let (key, value) = text.split_once('=').ok_or("no '=' in the line")?; if key != "port" { return Err(format!("unexpected key {key:?}").into()); } Ok(value.parse::<u16>()?)}
fn main() { for text in ["port=80", "port=eighty", "host=db", "port"] { match read_port(text) { Ok(p) => println!("{text}: {p}"), Err(e) => { // To react to one kind of failure, the caller has to guess the concrete type. if e.downcast_ref::<ParseIntError>().is_some() { println!("{text}: bad number ({e})"); } else { println!("{text}: other ({e})"); } } } }}port=80: 80 port=eighty: bad number (invalid digit found in string) host=db: other (unexpected key "host") port: other (no '=' in the line)
An error enum lists every failure in the signature, and the caller matches:
use std::fmt;use std::num::ParseIntError;
/// The signature lists every way this can fail.#[derive(Debug)]pub enum PortError { NoEquals, WrongKey(String), BadNumber(ParseIntError),}
impl fmt::Display for PortError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { PortError::NoEquals => write!(f, "no '=' in the line"), PortError::WrongKey(k) => write!(f, "unexpected key {k:?}"), PortError::BadNumber(e) => write!(f, "bad number: {e}"), } }}
impl std::error::Error for PortError {}
impl From<ParseIntError> for PortError { fn from(e: ParseIntError) -> PortError { PortError::BadNumber(e) }}
pub fn read_port(text: &str) -> Result<u16, PortError> { let (key, value) = text.split_once('=').ok_or(PortError::NoEquals)?; if key != "port" { return Err(PortError::WrongKey(key.to_string())); } Ok(value.parse::<u16>()?)}
fn main() { for text in ["port=80", "port=eighty", "host=db", "port"] { match read_port(text) { Ok(p) => println!("{text}: {p}"), Err(PortError::BadNumber(e)) => println!("{text}: bad number ({e})"), Err(e) => println!("{text}: other ({e})"), } }}port=80: 80 port=eighty: bad number (invalid digit found in string) host=db: other (unexpected key "host") port: other (no '=' in the line)
Box<dyn Error> is a fine choice in a main or a small program (Errors); Application errors shows the libraries that write the enum for you.
Translating from what you know
The layer 1 program is the parse-don’t-validate idea with an inherent Port::parse, in all nine languages. The blocks below cover newtypes, defaults and builders. Enums with data are compared in Enums, Option methods in Option, and cleanup in Common traits.
Going deeper
An idiom proves one thing
Each idiom that guards something (newtype, parse, enums, typestate, sealed) moves one rule into the compiler, and that rule is all it checks. A Port can be valid and the wrong port. A typestate guards the order of calls and not whether the server is up. A sealed trait stops outside implementations, not wrong ones inside the crate. Reach for an idiom when the rule is easy to break and costly to find later, and leave the rest to tests (Testing).
What an idiom costs
| Idiom | What you pay | Skip it when |
|---|---|---|
| Newtype | forwarding impls for what you need, no operators until added | the value lives in one function |
| Parse, don’t validate | a constructor and an error type | the check is one line, used once |
| Typestate | one type per state; run-time states need an enum | two states and one method |
| Builder | a mirror type and setters to keep in sync with the struct | two or three fields: use ..Default::default() |
| Guard | a type and a Drop impl; drop cannot return an error |
a plain close() call is enough |
| Extension trait | the trait must be imported at every use | a free function reads as well |
| Sealed trait | nobody downstream can add an implementation | you want plugins |
Where the guidelines are
The Rust API Guidelines checklist names these for reviews: C-NEWTYPE, C-BUILDER, C-CTOR (constructors are static, inherent methods), C-VALIDATE, C-SEALED and C-STRUCT-PRIVATE. Where to put these types, what to mark pub(crate), and how to split a library from a binary is in Project structure.
Practice
Try it
Replace each todo!() and run until it prints all checks passed. The starters compile and fail when run.
A temperature the type keeps valid
Write TryFrom<f64> for Celsius, as TryFrom<u32> for Port does above: NaN and both infinities are NotFinite, anything below -273.15 is BelowAbsoluteZero, and -273.15 itself is allowed. The field stays private. A check that only tests degrees < -273.15 compiles and fails, because NaN is not less than anything.
mod temperature { /// A temperature in degrees Celsius: a finite number, not below absolute zero (-273.15). /// The field is private, so `try_from` is the only way to make one. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Celsius(f64);
#[derive(Debug, PartialEq)] pub enum TempError { NotFinite, BelowAbsoluteZero, }
impl TryFrom<f64> for Celsius { type Error = TempError;
fn try_from(degrees: f64) -> Result<Celsius, TempError> { todo!("reject NaN and both infinities, then anything below -273.15") } }
impl Celsius { pub fn degrees(self) -> f64 { self.0 } }}
use temperature::{Celsius, TempError};
fn main() { assert_eq!(Celsius::try_from(21.5).unwrap().degrees(), 21.5); assert_eq!(Celsius::try_from(-273.15).unwrap().degrees(), -273.15); // the limit itself is allowed assert_eq!( Celsius::try_from(-273.16), Err(TempError::BelowAbsoluteZero) ); assert_eq!(Celsius::try_from(-1e9), Err(TempError::BelowAbsoluteZero)); assert_eq!(Celsius::try_from(f64::NAN), Err(TempError::NotFinite)); assert_eq!(Celsius::try_from(f64::INFINITY), Err(TempError::NotFinite)); assert_eq!( Celsius::try_from(f64::NEG_INFINITY), Err(TempError::NotFinite) ); // `try_into` comes for free with `TryFrom`. let boiling: Result<Celsius, TempError> = 100.0_f64.try_into(); assert_eq!(boiling.map(Celsius::degrees), Ok(100.0)); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
mod temperature { /// A temperature in degrees Celsius: a finite number, not below absolute zero (-273.15). /// The field is private, so `try_from` is the only way to make one. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Celsius(f64);
#[derive(Debug, PartialEq)] pub enum TempError { NotFinite, BelowAbsoluteZero, }
impl TryFrom<f64> for Celsius { type Error = TempError;
fn try_from(degrees: f64) -> Result<Celsius, TempError> { if !degrees.is_finite() { return Err(TempError::NotFinite); } if degrees < -273.15 { return Err(TempError::BelowAbsoluteZero); } Ok(Celsius(degrees)) } }
impl Celsius { pub fn degrees(self) -> f64 { self.0 } }}
use temperature::{Celsius, TempError};
fn main() { assert_eq!(Celsius::try_from(21.5).unwrap().degrees(), 21.5); assert_eq!(Celsius::try_from(-273.15).unwrap().degrees(), -273.15); // the limit itself is allowed assert_eq!( Celsius::try_from(-273.16), Err(TempError::BelowAbsoluteZero) ); assert_eq!(Celsius::try_from(-1e9), Err(TempError::BelowAbsoluteZero)); assert_eq!(Celsius::try_from(f64::NAN), Err(TempError::NotFinite)); assert_eq!(Celsius::try_from(f64::INFINITY), Err(TempError::NotFinite)); assert_eq!( Celsius::try_from(f64::NEG_INFINITY), Err(TempError::NotFinite) ); // `try_into` comes for free with `TryFrom`. let boiling: Result<Celsius, TempError> = 100.0_f64.try_into(); assert_eq!(boiling.map(Celsius::degrees), Ok(100.0)); println!("all checks passed");}all checks passed
A builder that checks at the end
Finish RequestBuilder: defaults, chained setters where the last call wins, and one build that returns MissingUrl, EmptyUrl (blank after trimming) or UnknownMethod (with the method as given). The known methods are GET, POST, PUT and DELETE in any case, and the built request holds the method in upper case. A setter does not judge: a bad method replaced before build is fine, so checking inside the setters fails.
#[derive(Debug, PartialEq)]pub struct Request { pub url: String, pub method: String, pub retries: u32,}
#[derive(Debug, PartialEq)]pub enum BuildError { MissingUrl, EmptyUrl, UnknownMethod(String),}
/// Collects settings; `build` checks them all and makes the `Request`.pub struct RequestBuilder { // add the fields you need}
impl RequestBuilder { /// Defaults: method "GET", 3 retries, no url yet. pub fn new() -> RequestBuilder { todo!("start with the defaults") }
pub fn url(self, url: &str) -> RequestBuilder { todo!("remember the url; checking it is build's job") }
/// Any case is accepted; the built request holds it in upper case. pub fn method(self, method: &str) -> RequestBuilder { todo!("remember the method as given") }
pub fn retries(self, retries: u32) -> RequestBuilder { todo!("remember the count") }
/// Errors: no url was set (MissingUrl); the url is blank after trimming (EmptyUrl); /// the method is not GET, POST, PUT or DELETE in any case (UnknownMethod, as given). pub fn build(self) -> Result<Request, BuildError> { todo!("check everything here and return the Request") }}
fn main() { let r = RequestBuilder::new().url("http://a").build().unwrap(); assert_eq!( r, Request { url: "http://a".into(), method: "GET".into(), retries: 3 } );
let r = RequestBuilder::new() .retries(0) .method("post") .url("http://b") .build() .unwrap(); assert_eq!( r, Request { url: "http://b".into(), method: "POST".into(), retries: 0 } );
// the last call wins let r = RequestBuilder::new() .url("http://a") .url("http://c") .retries(1) .retries(2) .build() .unwrap(); assert_eq!((r.url.as_str(), r.retries), ("http://c", 2));
assert_eq!( RequestBuilder::new().retries(5).build(), Err(BuildError::MissingUrl) ); assert_eq!( RequestBuilder::new().url(" ").build(), Err(BuildError::EmptyUrl) ); assert_eq!( RequestBuilder::new() .url("http://a") .method("fetch") .build(), Err(BuildError::UnknownMethod("fetch".into())) ); // only GET, POST, PUT and DELETE are known: other words are not assert_eq!( RequestBuilder::new() .url("http://a") .method("patch") .build(), Err(BuildError::UnknownMethod("patch".into())) ); let r = RequestBuilder::new() .url("http://a") .method("put") .build() .unwrap(); assert_eq!(r.method, "PUT");
// a setter does not judge: a bad method that is replaced before `build` is fine let r = RequestBuilder::new() .method("fetch") .method("Delete") .url("http://a") .build() .unwrap(); assert_eq!(r.method, "DELETE"); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
#[derive(Debug, PartialEq)]pub struct Request { pub url: String, pub method: String, pub retries: u32,}
#[derive(Debug, PartialEq)]pub enum BuildError { MissingUrl, EmptyUrl, UnknownMethod(String),}
/// Collects settings; `build` checks them all and makes the `Request`.pub struct RequestBuilder { url: Option<String>, method: String, retries: u32,}
impl RequestBuilder { /// Defaults: method "GET", 3 retries, no url yet. pub fn new() -> RequestBuilder { RequestBuilder { url: None, method: "GET".to_string(), retries: 3, } }
pub fn url(mut self, url: &str) -> RequestBuilder { self.url = Some(url.to_string()); self }
/// Any case is accepted; the built request holds it in upper case. pub fn method(mut self, method: &str) -> RequestBuilder { self.method = method.to_string(); self }
pub fn retries(mut self, retries: u32) -> RequestBuilder { self.retries = retries; self }
/// Errors: no url was set (MissingUrl); the url is blank after trimming (EmptyUrl); /// the method is not GET, POST, PUT or DELETE in any case (UnknownMethod, as given). pub fn build(self) -> Result<Request, BuildError> { let url = self.url.ok_or(BuildError::MissingUrl)?; if url.trim().is_empty() { return Err(BuildError::EmptyUrl); } let upper = self.method.to_uppercase(); if !["GET", "POST", "PUT", "DELETE"].contains(&upper.as_str()) { return Err(BuildError::UnknownMethod(self.method)); } Ok(Request { url, method: upper, retries: self.retries, }) }}
fn main() { let r = RequestBuilder::new().url("http://a").build().unwrap(); assert_eq!( r, Request { url: "http://a".into(), method: "GET".into(), retries: 3 } );
let r = RequestBuilder::new() .retries(0) .method("post") .url("http://b") .build() .unwrap(); assert_eq!( r, Request { url: "http://b".into(), method: "POST".into(), retries: 0 } );
// the last call wins let r = RequestBuilder::new() .url("http://a") .url("http://c") .retries(1) .retries(2) .build() .unwrap(); assert_eq!((r.url.as_str(), r.retries), ("http://c", 2));
assert_eq!( RequestBuilder::new().retries(5).build(), Err(BuildError::MissingUrl) ); assert_eq!( RequestBuilder::new().url(" ").build(), Err(BuildError::EmptyUrl) ); assert_eq!( RequestBuilder::new() .url("http://a") .method("fetch") .build(), Err(BuildError::UnknownMethod("fetch".into())) ); // only GET, POST, PUT and DELETE are known: other words are not assert_eq!( RequestBuilder::new() .url("http://a") .method("patch") .build(), Err(BuildError::UnknownMethod("patch".into())) ); let r = RequestBuilder::new() .url("http://a") .method("put") .build() .unwrap(); assert_eq!(r.method, "PUT");
// a setter does not judge: a bad method that is replaced before `build` is fine let r = RequestBuilder::new() .method("fetch") .method("Delete") .url("http://a") .build() .unwrap(); assert_eq!(r.method, "DELETE"); println!("all checks passed");}all checks passed
A guard that closes on each way out
run opens two handles, then finishes, fails with Err or panics. Write the Drop impl so both handles close in reverse order on all three paths, including a panic that unwinds (the stack is cleaned up as the panic travels). Closing by hand before each return fails the panic check.
use std::cell::RefCell;use std::panic::{self, AssertUnwindSafe};
type Events = RefCell<Vec<String>>;
/// A made-up resource. Opening it records `open <name>`; closing records `close <name>`.struct Handle<'a> { events: &'a Events, name: &'static str,}
impl<'a> Handle<'a> { fn open(events: &'a Events, name: &'static str) -> Handle<'a> { events.borrow_mut().push(format!("open {name}")); Handle { events, name } }}
impl Drop for Handle<'_> { fn drop(&mut self) { todo!("record `close <name>`, so every way out of a scope closes the handle") }}
#[derive(Clone, Copy)]enum Mode { Fine, Fail, Panic,}
fn run(events: &Events, mode: Mode) -> Result<(), String> { let _db = Handle::open(events, "db"); let _file = Handle::open(events, "file"); events.borrow_mut().push("work".to_string()); match mode { Mode::Fine => {} Mode::Fail => return Err("failed".to_string()), Mode::Panic => panic!("boom"), } events.borrow_mut().push("done".to_string()); Ok(())}
fn events_of(mode: Mode) -> (Vec<String>, String) { let events: Events = RefCell::new(Vec::new()); let outcome = panic::catch_unwind(AssertUnwindSafe(|| run(&events, mode))); let what = match outcome { Ok(Ok(())) => "ok", Ok(Err(_)) => "error", Err(_) => "panic", }; (events.into_inner(), what.to_string())}
fn main() { // Keep the expected "boom" quiet; show any other panic, such as a todo!(). panic::set_hook(Box::new(|info| { if info.payload().downcast_ref::<&str>() != Some(&"boom") { eprintln!("{info}"); } }));
let (events, what) = events_of(Mode::Fine); assert_eq!(what, "ok"); assert_eq!( events, [ "open db", "open file", "work", "done", "close file", "close db" ] );
let (events, what) = events_of(Mode::Fail); assert_eq!(what, "error"); assert_eq!( events, ["open db", "open file", "work", "close file", "close db"] );
let (events, what) = events_of(Mode::Panic); assert_eq!(what, "panic"); assert_eq!( events, ["open db", "open file", "work", "close file", "close db"] ); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::cell::RefCell;use std::panic::{self, AssertUnwindSafe};
type Events = RefCell<Vec<String>>;
/// A made-up resource. Opening it records `open <name>`; closing records `close <name>`.struct Handle<'a> { events: &'a Events, name: &'static str,}
impl<'a> Handle<'a> { fn open(events: &'a Events, name: &'static str) -> Handle<'a> { events.borrow_mut().push(format!("open {name}")); Handle { events, name } }}
impl Drop for Handle<'_> { fn drop(&mut self) { self.events .borrow_mut() .push(format!("close {}", self.name)); }}
#[derive(Clone, Copy)]enum Mode { Fine, Fail, Panic,}
fn run(events: &Events, mode: Mode) -> Result<(), String> { let _db = Handle::open(events, "db"); let _file = Handle::open(events, "file"); events.borrow_mut().push("work".to_string()); match mode { Mode::Fine => {} Mode::Fail => return Err("failed".to_string()), Mode::Panic => panic!("boom"), } events.borrow_mut().push("done".to_string()); Ok(())}
fn events_of(mode: Mode) -> (Vec<String>, String) { let events: Events = RefCell::new(Vec::new()); let outcome = panic::catch_unwind(AssertUnwindSafe(|| run(&events, mode))); let what = match outcome { Ok(Ok(())) => "ok", Ok(Err(_)) => "error", Err(_) => "panic", }; (events.into_inner(), what.to_string())}
fn main() { // Keep the expected "boom" quiet; show any other panic, such as a todo!(). panic::set_hook(Box::new(|info| { if info.payload().downcast_ref::<&str>() != Some(&"boom") { eprintln!("{info}"); } }));
let (events, what) = events_of(Mode::Fine); assert_eq!(what, "ok"); assert_eq!( events, [ "open db", "open file", "work", "done", "close file", "close db" ] );
let (events, what) = events_of(Mode::Fail); assert_eq!(what, "error"); assert_eq!( events, ["open db", "open file", "work", "close file", "close db"] );
let (events, what) = events_of(Mode::Panic); assert_eq!(what, "panic"); assert_eq!( events, ["open db", "open file", "work", "close file", "close db"] ); println!("all checks passed");}all checks passed
Read more
- Rust API Guidelines and Rust Design Patterns (idioms, patterns, anti-patterns).
- The Rust Book: Implementing an Object-Oriented Design Pattern (State, then typestate) and Advanced Traits (newtype).
- Essays: Parse, don’t validate and The Typestate Pattern in Rust.
- The standard library:
Default,TryFrom,Drop,PhantomData,Cow. - On this site: Working with the borrow checker, Common traits, Project structure, or the errors index.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.