Step 1 of 7
Constructors, Default and builders
Rust has no constructors in the language, no default arguments and no function overloading. Instead there are a few conventions every Rust developer recognizes, and following them makes your types feel familiar to your teammates.
fn new(...) -> Selffor the obvious way to create a value.- The
Defaulttrait for "a sensible starting value", combined with struct update syntax to change just a few fields. - The builder pattern when there are many optional settings, or when building can fail.
Default and struct update
struct Settings {
volume: u8,
fullscreen: bool,
name: String,
fps_limit: Option<u32>,
}
impl Default for Settings {
fn default() -> Self {
Settings { volume: 80, fullscreen: true, name: String::from("player"), fps_limit: None }
}
}
fn main() {
let s = Settings { volume: 30, fps_limit: Some(60), ..Default::default() };
println!("{} {} {} {:?}", s.volume, s.fullscreen, s.name, s.fps_limit);
let zeroes: (u32, String, Vec<i32>, bool, Option<u8>) = Default::default();
println!("{zeroes:?}");
}
30 true player Some(60)
(0, "", [], false, None)
..Default::default() fills every field you didn't write from Settings::default(). Most std types implement Default (0, empty string, empty collections, false, None), so #[derive(Default)] works whenever those zero values are right; write the impl by hand when they aren't. Clippy's new_without_default lint asks for a Default impl whenever you write a public new() without arguments, because generic code (and ..Default::default()) can use it.
Builders
An HTTP request has a URL and maybe a method, a timeout, headers, a body... A new with seven parameters is unreadable at the call site (new(url, "GET", 30, true, None, ...): which true is which?). A builder gives every option a named method:
struct Request {
url: String,
method: String,
timeout_secs: u32,
headers: Vec<(String, String)>,
}
struct RequestBuilder {
url: String,
method: String,
timeout_secs: u32,
headers: Vec<(String, String)>,
}
impl RequestBuilder {
fn new(url: &str) -> Self {
RequestBuilder { url: url.to_string(), method: String::from("GET"), timeout_secs: 30, headers: Vec::new() }
}
fn method(mut self, method: &str) -> Self {
self.method = method.to_string();
self
}
fn timeout(mut self, secs: u32) -> Self {
self.timeout_secs = secs;
self
}
fn header(mut self, name: &str, value: &str) -> Self {
self.headers.push((name.to_string(), value.to_string()));
self
}
fn build(self) -> Result<Request, String> {
if !self.url.starts_with("https://") {
return Err(format!("url must use https: {}", self.url));
}
if self.timeout_secs == 0 {
return Err(String::from("timeout must be positive"));
}
Ok(Request { url: self.url, method: self.method, timeout_secs: self.timeout_secs, headers: self.headers })
}
}
fn main() {
let req = RequestBuilder::new("https://api.arena.dev/scores")
.method("POST")
.timeout(5)
.header("Accept", "application/json")
.build()
.expect("valid request");
println!("{} {} ({}s, {} header)", req.method, req.url, req.timeout_secs, req.headers.len());
match RequestBuilder::new("http://insecure.example").build() {
Ok(_) => println!("built"),
Err(e) => println!("error: {e}"),
}
}
POST https://api.arena.dev/scores (5s, 1 header)
error: url must use https: http://insecure.example
Piece by piece:
- Each setter takes
mut selfby value and returnsSelf, so calls chain:new(..).method(..).timeout(..). This is the consuming builder, the most common style. build(self)checks the combination and returnsResult, so an invalidRequestcan never exist. Validation lives in one place.- Defaults (
GET, 30 seconds) live innew, and the call site only mentions what differs. - The standard library has builders too.
std::thread::Builderis consuming, like this one.std::process::Command::new("ls").arg("-l")andOpenOptions::new().append(true).open(..)use the&mut selfstyle (fn arg(&mut self, ..) -> &mut Self), which also lets you configure conditionally:if verbose { cmd.arg("-v"); }.
Common mistakes
Forgetting .build(): you're holding a builder, not the thing you wanted.
struct Request {
url: String,
}
struct RequestBuilder {
url: String,
}
impl RequestBuilder {
fn new(url: &str) -> Self {
RequestBuilder { url: url.to_string() }
}
fn build(self) -> Request {
Request { url: self.url }
}
}
fn send(req: Request) {
println!("sending to {}", req.url);
}
fn main() {
send(RequestBuilder::new("https://arena.dev"));
}
error[E0308]: mismatched types
- A builder for a two-field struct is ceremony. Use one when there are several optional settings or building needs validation.
- Setters that take
selfbut forget to return it: the chain breaks withno method named `timeout` found for unit type `()`.
Your task
Write a builder for Server. Server::builder() returns a ServerBuilder with defaults host 127.0.0.1, port 8080, workers 4, tls false. Add consuming setters host, port, workers and tls, and build() returning Result<Server, String> with the errors port must not be 0 and workers must be 1 to 64 (check the port first).
Challenge 2
Implement Default for LogConfig (level "info", color true, max_files 5, path "app.log"), then write fn debug_config() -> LogConfig that returns the defaults with only level set to "debug" and max_files to 20, using struct update syntax.
Challenge 3
Write a consuming builder that produces SQL text. QueryBuilder::new("users") starts a query; .filter(cond) can be called several times (conditions are joined with AND), .order_by(column) and .limit(n) are optional. .build() returns the query string, for example SELECT * FROM users WHERE age > 18 AND active ORDER BY name LIMIT 10. Leave out the parts that weren't set.