Many modern languages have a built-in feature called "named function arguments". This is how it looks in Python, for example:
def greet(name: str, age: int) -> str:
return f"Hello {name} with age {age}!"
greeting = greet(
# Notice the `key = value` syntax at the call site
name="Bon",
age=24,
)
assert greeting == "Hello Bon with age 24!"
This feature allows you to associate the arguments with their names when you call a function. It improves readability, API stability, and maintainability of your code.
It's just such a nice language feature, which is not available in Rust, unfortunately. However, there is a way to work around it, that I'd like to share with you.
Naive Rust solution
Usually, Rust developers deal with this problem by moving their function parameters into a struct. However, this is inconvenient, because it's a lot of boilerplate, especially if your function arguments contain references:
// Meh, we need to annotate all lifetime parameters here
struct GreetParams<'a> {
name: &'a str,
age: u32
}
fn greet(params: GreetParams<'_>) -> String {
// We also need to prefix each parameter with `params` or manually
// destructure the `params` at the start of the function.
format!("Hello {} with age {}!", params.name, params.age)
}
// Ugh... consumers need to reference the params struct name here,
// which usually requires an additional `use your_crate::GreetParams`
greet(GreetParams {
name: "Bon",
age: 24
});
This situation becomes worse if you have more references in your parameters or if your parameters need to be generic over some type. In this case, you need to duplicate the generic parameters both in your function and in the parameters' struct declaration. Also, the convenient function's impl Trait
syntax is unavailable with this approach.
The other caveat is that the struct literal syntax doesn't entirely solve the problem of omitting optional parameters. There is a clunky way to have optional parameters by requiring the caller to spread the Default
parameters struct instance in the struct literal.
struct GreetParams<'a> {
name: &'a str,
age: u32,
// Optional
job: Option<String>
}
GreetParams {
name: "Bon",
age: 24,
// But.. this only works if your whole `GreetParams` struct
// can implement the `Default` trait, which it can't in this
// case because `name` and `age` are required
..GreetParams::default()
}
This situation can be slightly improved if you derive a builder for this struct with typed-builder
, for example. However, typed-builder
doesn't remove all the boilerplate. I just couldn't stand, but solve this problem the other way.
Better Rust solution
I've been working on a crate called bon
that solves this problem. It allows you to have almost the same syntax for named function arguments in Rust as you'd have in Python. bon
generates a builder directly from your function:
#[bon::builder]
fn greet(name: &str, age: u32) -> String {
format!("Hello {name} with age {age}!")
}
let greeting = greet()
.name("Bon")
.age(24)
.call();
assert_eq!(greeting, "Hello Bon with age 24!");
def greet(name: str, age: int) -> str:
return f"Hello {name} with age {age}!"
greeting = greet(
name="Bon",
age=24,
)
assert greeting == "Hello Bon with age 24!"
We only placed #[bon::builder]
on top of a regular Rust function. We didn't need to define its parameters in a struct
or write any other boilerplate. In fact, this Rust code is almost as succinct as the Python version. Click on Python
at the top of the code snippet above to compare both versions.
I hope you didn't break that button 🐱
The builder generated by bon
allows the caller to omit any parameters that have Option
type. That part is covered.
It supports almost any function syntax. It works fine with functions inside of impl
blocks, async
functions, generic parameters, lifetimes (including implicit and anonymous lifetimes), impl Trait
syntax, etc.
Furthermore, it also never panics. All errors such as missing required arguments are compile-time errors ✔️.
bon
allows you to easily switch your regular function into a function "with named parameters" that returns a builder. Just adding #[builder]
to your function is enough even in advanced use cases.
One builder crate to rule them all
bon
was designed to cover all your needs for a builder. It works not only with functions and methods, but it also works with struct
s as well via a #[derive(Builder)]
:
use bon::Builder;
#[derive(Builder)]
struct User {
id: u32,
// This attribute makes the setter accept `impl Into<String>`
// which lets us pass an `&str` directly and it'll be automatically
// converted into `String`.
#[builder(into)]
name: String,
}
User::builder()
.id(1)
.name("Bon")
.build();
So, you can use just one builder crate solution consistently for everything. Builders for functions and structs both share the same API design, which allows you, for example, to switch between a #[derive(Builder)]
on a struct and a #[builder]
attribute on a method that creates a struct. This won't be an API-breaking change for your consumers (details).
Summary
If you'd like to know more about bon
and use it in your code, then visit the guide page with the full overview.
bon
is fully open-source, and available on crates.io. Its code is public on Github. Consider giving it a star ⭐ if you liked the idea and the implementation.
Acknowledgements
This project was heavily inspired by such awesome crates as buildstructor
, typed-builder
and derive_builder
. bon
crate was designed with many lessons learned from them.
TIP
You can leave comments for this post on Reddit.