Conditional Building
On this page, we'll review a case when you have multiple branches in your code that need to set different values for different builder members.
Since builders generated by bon
use the typestate pattern and setters consume self
, it is a bit more complicated for conditional code to use them. But, not until you know the patterns described below 🐱. So let's learn how to write better conditional code 📚.
The patterns described here aren't mutually exclusive. You can combine them as you see necessary to keep your code clean.
Shared partial builder
If your conditional code needs to set the same values for the same members, consider extracting this shared code into a variable that holds a partial builder.
Example:
use bon::Builder;
#[derive(Builder)]
#[builder(on(String, into))]
struct User {
name: String,
tags: Vec<String>,
alias: Option<String>,
description: Option<String>,
}
// Common part is here
let user = User::builder()
.name("Bon")
.tags(vec!["confectioner".to_owned()]);
// Differences are in each branch
let user = if 2 + 2 == 4 {
user
.alias("Good girl")
.description("Knows mathematics 🐱")
.build()
} else {
user
.description("Skipped math classes 😿")
.build()
};
It's important to call the build()
method inside each of the branches of conditional code. This way every branch returns the value of the same type.
This also works with match
expressions. Just remember to call .build()
in every arm.
Deep Thought 👀
This pattern of branching and converging on the same code path can be described using the terms "upcasting" or "context loss".
The part of the code inside of the conditional branches has more context than the code that comes after. The code inside of the conditional block has a strongly typed builder that encodes the info that, for example, the alias
member was set (and thus we can no longer call this setter).
Once the build()
method is called, we no longer have the context of how exactly the User
was built.
Shared total builder
In contrast to the shared partial builder, here we'll use the builder strictly after the conditional code. The conditional code needs to create the variables that hold the component values for the builder beforehand.
Example:
use bon::Builder;
#[derive(Builder)]
#[builder(on(String, into))]
struct User {
name: String,
tags: Vec<String>,
alias: Option<String>,
description: Option<String>,
}
let knows_math = 2 + 2 == 4;
// Differences are in branches for each variable
let alias = if knows_math {
Some("Good girl")
} else {
None
};
// ^^^^ This can be reduced to a single line with `bool::then_some()`
// (try it yourself)
let description = if knows_math {
"Knows mathematics 🐱"
} else {
"Skipped math classes 😿"
};
// Common part is here
let user = User::builder()
.name("Bon")
.tags(vec!["confectioner".to_owned()])
// Use the `maybe_` setter to provide an `Option<T>`
.maybe_alias(alias)
// If the description could be `None`, we'd use `maybe_description`,
// but in this case, all branches set the `description`, so we can
// use this shorter setter that wraps with `Some()` internally
.description(description)
.build();
In this case, we create a variable for each conditional member beforehand and initialize them separately, then we pass the results to the builder. We benefit from the maybe_
setters for optional members such that we can pass the Option<T>
values directly.
NOTE
Creating separate variables is not strictly required. You can inline the usages of variables in such simple code as here, where each branch of the if
takes a single line. Anyhow, branches can be much bigger in real code.
If you'd like to avoid checking the same condition multiple times, you can write a single if
that returns a tuple of values for several members:
let (alias, description) = if 2 + 2 == 4 {
(Some("Good girl"), "Knows mathematics 🐱")
} else {
(None, "Skipped math classes 😿")
};
However, when the number of members in the conditional logic grows, such an if
with tuples becomes hard to read. In such a case, consider defining variables outside of the if
or using the shared partial builder pattern instead.
Example:
let mut alias = None;
let description;
if 2 + 2 == 4 {
alias = Some("Good girl");
description = "Knows mathematics 🐱";
} else {
description = "Skipped math classes 😿";
}
// Pass the variables to the builder now
// ...