Step 6 of 7
Method chaining
You've chained calls before: line.trim().to_uppercase() calls to_uppercase on whatever trim returned. Your own methods can be designed for chaining too, by returning the value itself.
Builders: take self, return Self
A method that takes mut self and returns Self changes the value and hands it back, ready for the next call:
struct Request {
url: String,
method: String,
timeout_secs: u32,
retries: u32,
}
impl Request {
fn get(url: &str) -> Self {
Self {
url: url.to_string(),
method: String::from("GET"),
timeout_secs: 30,
retries: 0,
}
}
fn timeout(mut self, secs: u32) -> Self {
self.timeout_secs = secs;
self
}
fn retries(mut self, n: u32) -> Self {
self.retries = n;
self
}
fn describe(&self) -> String {
format!("{} {} (timeout {}s, {} retries)", self.method, self.url, self.timeout_secs, self.retries)
}
}
fn main() {
let req = Request::get("https://example.com/api")
.timeout(5)
.retries(3);
println!("{}", req.describe());
let quick = Request::get("https://example.com/health");
println!("{}", quick.describe());
}
GET https://example.com/api (timeout 5s, 3 retries)
GET https://example.com/health (timeout 30s, 0 retries)
mut selfmeans "I own this value, and I'll change it". It's the samemutyou'd put on any parameter: the caller doesn't need amutvariable, because ownership moves into the method.- The last line,
self, returns the value so the next method can take it. - Settings you don't call keep their defaults (
quickhas 30 seconds and 0 retries). That's why this style is popular for configuration with many optional settings: it's the builder pattern, used by HTTP clients, CLI parsers and database drivers. You'll study it more in the design patterns module. cargo fmtputs each call of a long chain on its own line, starting with the dot, as above.
Changing in place: take &mut self, return &mut Self
When you want to keep a variable and change it several times, return &mut Self instead:
struct Tally {
total: u32,
count: u32,
}
impl Tally {
fn add(&mut self, value: u32) -> &mut Self {
self.total += value;
self.count += 1;
self
}
}
fn main() {
let mut t = Tally { total: 0, count: 0 };
t.add(5).add(10).add(20);
println!("{} values, total {}", t.count, t.total);
}
3 values, total 35
Here self is already a &mut Tally, so returning self hands the same mutable borrow to the next call.
Common mistakes
- Forgetting to return
self: the method then returns(), and the compiler sayserror[E0308]: mismatched typeswithexpected `Request`, found `()`. End the method withself. - Forgetting
mutonself:error[E0594]: cannot assign to `self.timeout_secs`, as `self` is not declared as mutable. Writemut self.
Your task
Write a builder for alert messages. Message::new(to) starts with an empty subject and urgent false. subject(text) sets the subject and urgent() marks the message as urgent; both take mut self and return Self. preview builds a one-line summary:
subject -> to, for exampleDisk full -> [email protected]- with
[URGENT]in front when urgent:[URGENT] Disk full -> [email protected] (no subject)instead of the subject when it's empty (self.subject.is_empty()is true for an empty String).
Challenge 2
Write Stats, which records numbers in place with chainable calls:
fn new() -> Self: no values yet: count 0, total 0,highestset toi32::MIN.fn record(&mut self, value: i32) -> &mut Self: counts the value, adds it tototal, updateshighest, and returnsselffor chaining.fn average(&self) -> i32:total / count(integer division), or 0 when nothing has been recorded.
Challenge 3
Write a pizza order builder from scratch:
fn new(size_cm: u32) -> Self: no toppings, no extra cheese.fn topping(mut self, name: &str) -> Self: adds a topping (you can add several).fn extra_cheese(mut self) -> Selffn price_cents(&self) -> u32: 800 for a pizza up to 30 cm, 1100 above that, plus 150 per topping, plus 100 for extra cheese.
Previous: Getters, invariants and multiple impl blocks Next: Putting it together: a bank account program