Rust has a lot of benefits.
But unlike some other programming languages, Rust doesn't have either:
This means that if you want to get the most from this language, you'll need to figure out a way to create a new instance of a structure in the most ergonomic way possible.
Don't worry though. In this guide, we'll explore different techniques and patterns available for instantiating structures in the Rust programming language.
Sidenote: If you struggle with either understanding or applying these techniques, want to learn more, or simply want a deep dive into learning Rust (regardless of current level), then check out my complete Rust course.
Learn how to code and build your own real-world applications using Rust so that you can get hired this year. No previous programming or Rust experience needed.
You can check out the course here, or watch the first few videos for free.
Better still. You also get direct access to me, other Rust students, and working Rust developers via our private Discord channel! You can ask questions, get feedback and more.
Anyways, with that out of the way, let's get into this guide.
We'll start with the technique that takes the fewest amount of steps and is the quickest to write, which is implementing a new
function.
Using a new
function has two major benefits:
Result<T>
to signal possible instantiation errorsLet's take a look at a sample new
function below:
struct User {
name: String,
}
impl User {
// Here's a `new` function to make a new `User`.
// The generics allow using `String` or `&str` as an argument.
pub fn new<S: Into<String>>(name: S) -> Self {
Self { name: name.into() }
}
}
let alice = User::new("Alice");
let bob = User::new(String::from("Bob"));
There's not much happening here, but let's break it down anyway.
The new
function has a name
parameter, creates a new User
having name
, and then returns the User
.
Simple! But what about when you need to support situations where instantiation might fail?
Well, then you can change the return type to Result
, like so:
struct User {
name: String,
// Now we have an email
email: String,
}
impl User {
pub fn new<S: Into<String>(name: S, email: S) -> Result<Self, &'static str> {
let email = email.into();
// The email might be malformed, so let's check it with an imaginary validation function
if email_is_ok(&email) {
Ok(Self {
name: name.into(),
email,
})
} else {
Err("malformed email")
}
}
}
let alice = User::new("Alice", "alice@example.com")?;
Using a new
function works great for small structures, but it doesn't scale well. Each time another field gets added to User
, we need to add a new parameter to the new
function.
It will eventually reach a point where the function will have too many parameters, making it problematic to work with:
// changing requirements demand an authorization level
enum AuthLevel {
Admin,
Guest,
User,
}
struct User {
name: String,
email: String,
// more fields!
auth_level: AuthLevel,
address: Option<String>,
phone: Option<String>,
}
impl User {
// function signature isn't _too_ bad, but it does take extra effort to comprehend
pub fn new<S: Into<String>>(
name: S,
email: S,
auth_level: AuthLevel,
address: Option<S>,
phone: Option<S>,
) -> Result<Self, &'static str> {
todo!()
}
}
// calling `new` is becoming too verbose
let alice = User::new(
"Alice",
"alice@example.com",
AuthLevel::User,
Some("+1-321-555-1234"),
None,
);
Also, you might have noticed that the phone
and address
got swapped, and now those fields in the structures will have incorrect data.
Not great, and yet another reason why it's a good idea to avoid a large number of function parameters.
Well, it's pretty simple. Introducing new types both fixes the swapped data problem, and allows us to make a new User
without Result
.
Better still, it makes calling the function even more verbose than it already is:
impl User {
pub fn new(
name: Name,
email: Email,
auth_level: AuthLevel,
address: Option<Address>,
phone: Option<Phone>,
) -> Self {
todo!()
}
}
let alice = User::new(
Name::new("Alice"),
Email::new("alice@example.com")?,
AuthLevel::User,
None,
Some(Phone::new("+1-321-555-1234")),
);
Now, although this solution works, it still isn't ideal and has a few issues.
Sure, we could move the instantiation of the new types to individual variable bindings, but that would double the lines of code.
And so before trying to make this more ergonomic, we should first take a look at what other techniques are available for instantiating structures...
Fluent interfaces combine method chaining with English prose to create an API that reads naturally.
To enable method chaining in Rust, we need to implement functions that return Self
.
This will expose the implemented methods on the structure and we can continually call whichever methods we need.
For example
Let's start with a structure for configuring an HttpRequest
:
// Boilerplate-y stuff
struct Cookie;
enum HttpMethod {
Get,
Post,
Put,
}
// Struct we will use to implement the fluent interface
struct HttpRequest {
address: String,
method: HttpMethod,
cookies: Vec<Cookie>,
timeout: std::time::Duration,
body: Vec<u8>,
}
// The important bits
impl HttpRequest {
// `address` is required
pub fn new<S: Into<String>>(address: S) -> Self {
Self {
address: address.into(),
// set the defaults for everything else
method: HttpMethod::Get,
cookies: vec![],
timeout: std::time::Duration::from_secs(5),
body: vec![],
}
}
}
Since new
returns Self
, we can begin calling more methods implemented on HttpRequest
using method syntax.
Important: While we can technically name these methods anything we want, to make the interface fluent we should prefix the names with transitive verbs or
prepositions, as doing so helps to make the method chaining read more fluently.
We'll start by using with_
as the prefix on the method names:
impl HttpRequest {
// Consume `self` and then return `Self`. This enables method chaining
// because the return value is also `HttpRequest`, which implements these
// methods.
pub fn with_body(self, body: Vec<u8>) -> Self {
// since we own self, we can make it mutable:
let mut req = self;
// set the body:
req.body = body;
// move `req` back out of the method:
req
}
pub fn with_method(self, method: HttpMethod) -> Self {
let mut req = self;
req.method = method;
req
}
pub fn with_timeout(self, timeout: std::time::Duration) -> Self {
let mut req = self;
req.timeout = timeout;
req
}
}
Other good candidates for names in a fluent interface are using_
, use_
, and include_
.
I'll use add_
for adding cookies to the cookies
field:
impl HttpRequest {
pub fn add_cookie(self, cookie: Cookie) -> Self {
let mut req = self;
req.cookies.push(cookie);
req
}
}
Now that we have implemented a fluent interface, we can use it like this:
let request = HttpRequest::new("example.com")
.with_method(HttpMethod::Post)
.add_cookie(Cookie)
.add_cookie(Cookie)
.with_body("sample".into());
Since the initial call to new
creates an HttpRequest
to work with, implementing a fluent interface is more suitable when the fields have reasonable defaults, so let's take a look at them.
When your data has default values for every field, we can use the Default
trait from the standard library.
Sidenote:
Default
allows setting default values to the fields of a structure. However, Rust doesn't implement this automatically, but we can derive it.
To show how Default
works, we'll create a Color
structure which consists of red, green, and blue color components:
// automatically implement Default
#[derive(Default)]
struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
// the default for a `u8` is 0
let black = Color::default();
This is fine if we don't mind setting all the fields to 0
, but if we introduce an alpha channel to control transparency, then we should implement Default
manually.
Doing so allows us to set the default alpha channel to 255
, which is fully opaque:
struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
// implement Default manually
impl Default for Color {
fn default() -> Self {
Self {
r: 0,
g: 0,
b: 0,
a: 255, // now we can see the color!
}
}
}
Using Default
works great when all the fields of your struct have reasonable default values.
When combined with the technique in the next section, it offers an ergonomic way to create structures having different initial values.
Rust's struct update syntax enables us to take the fields from one instance of a structure and apply them to the fields of another structure.
Doing this reduces the number of steps needed to make copies of something where each copy has a slight alteration.
Using the Color
example from the previous section:
// partially transparent black
let half_transparent = Color {
r: 0,
g: 0,
b: 0,
a: 127,
};
let transparent_red = Color {
r: 255,
// use `struct update` (..) to set the other fields
// to the same value as `half_transparent`
..half_transparent
};
Struct update syntax also works with the Default
trait:
let green = Color {
g: 255,
// set the other fields to default values
..Default::default()
};
The downside with struct update syntax is it works only when all of the fields are public.
Struct update automatically expands and sets the fields, so if they are private we won't have access to set them and we'll get a compiler error.
Default
is great, but not all structures have default values that make sense for every field. (Or perhaps we do have good defaults, but want to keep some fields private).
In either case, struct update won't work, but there is a solution. We can instead create a new structure containing all the public and default options.
For example
Let's create a Primitive
structure for use in 3d modeling:
struct Primitive {
model: Model,
material: Option<Material>,
position: Vec3,
rotation: Vec3,
scale: Vec3,
}
We could derive Default
here, but I'd like to set the initial Model
when creating the Primitive
, and so Default
won't work in this case.
Instead, we can create a Config
structure and move all the values that
do have sensible defaults to this Config
structure, and then derive Default
:
#[derive(Default)]
struct PrimitiveConfig {
material: Option<Material>,
position: Vec3,
rotation: Vec3,
scale: Vec3,
}
struct Primitive {
model: Model,
config: PrimitiveConfig,
}
First we will implement some functions to create a new Primitive
:
impl Primitive {
// make a new Primitive with the given config
pub fn new(model: Model, config: PrimitiveConfig) -> Self {
Self { model, config }
}
// make a new Primitive using the default config
pub fn new_with_default(model: Model) -> Self {
Self {
model,
config: Self::default_config(),
}
}
}
We should also implement a function to get the configuration structure with all defaults set.
Why?
Well, implementing this directly on Primitive
makes it easier to work with the PrimitiveConfig
since we don't need to include any extra use
statements:
impl Primitive
// return a fresh default config
pub fn default_config() -> PrimitiveConfig {
PrimitiveConfig::default()
}
}
We could implement a fluent interface on PrimitiveConfig
to set the fields for configuring the Primitive
. However, we won't be able to use method chaining when we use Primitive::new()
.
Instead, we can implement a fluent interface on Primitive
and have it update the configuration for us:
impl Primitive {
pub fn with_position(self, position: Vec3) -> Self {
let mut obj = self;
obj.config.position = position;
obj
}
pub fn with_scale(self, scale: Vec3) -> Self {
let mut obj = self;
obj.config.scale = scale;
obj
}
pub fn with_rotation(self, rotation: Vec3) -> Self {
let mut obj = self;
obj.config.rotation = scale;
obj
}
}
With this functionality implemented, we now have access to some different ways to create a new Primitive
.
Using the fluent interface looks like this:
// new Sphere using defaults
let sphere = Primitive::new_with_default(Sphere);
// new Sphere using defaults with modified position and scale
let sphere = Primitive::new_with_default(Sphere)
.with_position(Vec3(10.0, 150.0, 0.0))
.with_scale(Vec3(2.0, 2.0, 2.0));
If we want to make heavy modifications to the configuration, we can create one ahead of time and use that with the new
function:
// make a config struct with lots of changes
let sphere_config = PrimitiveConfig {
position: Vec3(100.0, 50.0, 0.0),
scale: Vec3(2.0, 2.0, 2.0),
material: Material(Stone),
// use defaults for everything else
..Default::default()
};
// new Sphere using config from above
let sphere = Primitive::new(Sphere, sphere_config);
The configuration can get contained within a single expression by inlining it's creation in the Primitive::new()
call:
// new Sphere with inline config
let sphere = Primitive::new(
Sphere,
PrimitiveConfig {
position: Vec3(100.0, 50.0, 0.0),
// use default material, rotation, and scale
..Default::default()
},
);
And we can also copy configuration from another Primitive
:
let cube = Primitive::new(
Cube,
PrimitiveConfig {
scale: Vec3(5.0, 5.0, 5.0),
// use the sphere settings for everything else
..sphere.config
},
);
Combining the Default
trait with a fluent interface offers a convenient way to create structures.
They are most useful when the structure consists of fields that have sensible defaults. In situations where there are many fields with no reasonable default values, we will need to use another approach.
In the fluent interface example, we created a new structure right away and then used implemented methods to set the fields.
This works fine when most of the fields have a reasonable default. but if we have a structure containing many fields without defaults, then the fluent interface can't get implemented directly on the structure.
We first would need to set the non-default fields in the new
function and this leads to the verbose API, which we saw at the beginning of this guide.
Instead we'll implement the fluent interface on a dedicated "builder" structure and then incrementally "build" the structure by setting one field at a time.
Why do this?
Well, the builder structure contains all the same fields as the target structure, but with two main differences:
Default
implementation get wrapped in Option
Default
This enables us to instantiate the builder structure and then set the required values (and possibly override the defaults) using the fluent interface.
Once we have set the values, we then call a "build" method which returns the target structure with all the fields set.
For example
Let's apply this to the User
structure from the beginning of this post, since it has some fields without defaults:
// the User struct, for reference
struct User {
name: String,
email: String,
auth_level: AuthLevel,
address: Option<String>,
phone: Option<String>,
}
impl User {
// convenience function to get a new builder
pub fn builder() -> UserBuilder {
UserBuilder::default()
}
}
// the "builder" struct
#[derive(Default)]
struct UserBuilder {
name: Option<String>,
email: Option<String>,
auth_level: AuthLevel,
address: Option<String>,
phone: Option<String>,
}
// We need to implement `Default` on `AuthLevel` so we can
// derive `Default` on `UserBuilder`. Using `Guest` is a
// sensible default.
impl Default for AuthLevel {
fn default() -> Self {
AuthLevel::Guest
}
}
Now we can implement the builder methods on UserBuilder
. However, instead of returning Self
like we did in the previous sections, we will return &mut Self
.
This allows us to continually change the values without making copies of the builder. (Either way works fine with a builder).
A quick note on naming:
with_
prefix (or something similar) to create a fluent interfaceself
to whatever gets passed into the methodHere's the implementation:
impl UserBuilder {
pub fn name<S: Into<String>>(&mut self, name: S) -> &mut Self {
self.name = Some(name.into());
self
}
pub fn email<S: Into<String>>(&mut self, email: S) -> &mut Self {
self.email = Some(email.into());
self
}
// .. snip ..
// the remaining fields follow the same pattern
// .. snip ..
// Creates the target User structure
pub fn build(self) -> Result<User, &'static str> {
let name = self.name.ok_or("must have a name")?;
let email = self.email.ok_or("must have an email")?;
Ok(User {
name,
email,
auth_level: self.AuthLevel,
address: self.address,
phone: self.phone,
})
}
}
We also created a build
method which instantiates the User
struct.
(It returns a Result
because it needs to verify that all the necessary fields were populated).
This is the difference between the fluent interface from the previous sections and the builder pattern. Builders can verify that the fields have appropriate data, before returning the target structure.
Pretty helpful right?
To use the builder:
let mut alice = User::builder();
alice
.name("Alice")
.email("alice@example.com")
.auth_level(AuthLevel::User)
.phone("+1-321-555-1234");
// optional type annotations show the type we get back from .build()
let alice: Result<User, &'static str> = alice.build()?;
Important: Although this technique works for all structures regardless of the number of fields that have do (or don't) have defaults, it does have a disadvantage.
Since the fields get checked at runtime, there is runtime overhead to using a builder. It won't always be an issue, but if you are using a builder in a hot loop, then there may be a reduction in performance, so keep it in mind.
A typed builder combines the type state pattern with the builder pattern.
This gives us a builder that gets checked for errors at compile time instead of at runtime, making the typed builder a zero-cost abstraction.
For a more detailed breakdown of how this works, check out my guide on the type state pattern in Rust.
Builders need to set some (or all) fields using implemented methods, so the states we need for each field are:
We'll get started with a trimmed-down User
structure using the name
and email
fields:
struct User {
name: String,
email: String,
}
Since there are two fields, we need 4 states: a "set" state and "not set" state for both name
and email
.
We'll make each "set" state a new type which will contain the data needed by User
:
// states for the `name` field
struct Name(String);
struct NoName;
// states for the `email` field
struct Email(String);
struct NoEmail;
Next we'll create a generic builder structure:
struct UserBuilder<N, E> {
name: N,
email: E,
}
The UserBuilder
structure is generic over N
and E
:
N
is the current state of the name
fieldE
is the current state of the email
fieldThe idea with this structure is to set the generic parameters to the current state of the building process.
Here are some type annotations showing all possible states of the builder:
let no_fields_set: UserBuilder<NoName, NoEmail>;
let email_field_set: UserBuilder<NoName, Email>;
let name_field_set: UserBuilder<Name, NoEmail>;
let both_fields_set: UserBuilder<Name, Email>;
If we were to implement functionality on UserBuilder<NoName, NoEmail>
then the function we write will be available exclusively when we have not set any fields.
This is the perfect spot for a new
function because we will start the state machine with both fields in the "not set" state:
impl UserBuilder<NoName, NoEmail> {
pub fn new() -> Self {
Self {
name: NoName,
email: NoEmail,
}
}
}
Now comes the fun part!
We need to write impl
blocks for the remaining combinations of states, however, when we create these implementation blocks, we may transition the state for only a single field at a time.
For example
When setting the value of the email
field, the state needs to transition
from NoEmail
to Email
. This means we want to write our implementation block to ignore the name
field.
We will use the generic parameter N
to represent the state of the name
field. (N
can be any state and we don't care what the state is).
Also, this implementation block is only for managing the state of the email
field:
// In our example, this applies to:
// - UserBuilder<Name, NoEmail>
// - UserBuilder<NoName, NoEmail>
// And also applies to:
// - UserBuilder<N, NoEmail>
// Where `N` is anything
//
// ┌── Transition from `NoEmail` to `Email` ────┐
// │ │
impl<N> UserBuilder<N, NoEmail> { // │
// │ │
// └─ N remains unchanged ──────────────────────┐ │
// │ │
pub fn email<S: Into<String>>(self, email: S) -> UserBuilder<N, Email> {
UserBuilder {
// We move whatever state was in the `name` field to the new
// `UserBuilder`.
name: self.name,
// Change the state from `NoEmail` to `Email`. Since it's
// a new type, we include the `email` as a String.
email: Email(email.into()),
}
}
}
To implement the state transition for name
, we can copy the above block and change the generic parameters and fields accordingly:
// ┌── Transition from `NoName` to `Name` ────┐
// │ │
impl<E> UserBuilder<NoName, E> { // │
pub fn name<S: Into<String>>(self, name: S) -> UserBuilder<Name, E> {
UserBuilder {
name: Name(name.into()),
email: self.email,
}
}
}
Now that we have methods to set the name
and email
fields, we can write the build
method to construct a User
whenever the UserBuilder
has both the name
and email
fields in the "set" state.
The "set" states are the Name
and Email
structures, so the implementation block must use UserBuilder<Name, Email>
:
// `Name` and `Email` are the "set" states. The `build` method is
// available solely when the builder is at this state.
impl UserBuilder<Name, Email> {
pub fn build(self) -> User {
User {
// The states are tuple structs, using
// .0 accesses the inner String.
name: self.name.0,
email: self.email.0,
}
}
}
After implementing a convenience function on the User
struct, we can use the typed builder like we did with the builder in the previous section:
impl User {
// Create a builder having the states `NoName` and `NoEmail`
pub fn builder() -> UserBuilder<NoName, NoEmail> {
UserBuilder::new()
}
}
// optional type annotation shows the type we get back from .build()
let alice: User = User::builder()
.name("Alice")
.email("alice@example.com")
.build();
Sidenote: With the typed builder implemented, if we forget to call .name()
or .email()
, then we will get a compiler error.
However, we will also get a compiler error if we call these functions more than a single time.
In either instance we'll get an error that looks something like this:
As you will agree, this isn't the most helpful error, but we can fix that!
Now that we have seen how the typed builder pattern works, let's jump straight to the typed_builder crate.
This crate uses procedural macros to:
Since we don't need to write any of the implementation blocks, let's go back to using the original User
struct with all it's fields:
struct User {
name: String,
email: String,
auth_level: AuthLevel,
address: Option<String>,
phone: Option<String>,
}
The typed_builder
crate provides field annotations that we can use to customize how the impl
blocks get generated.
This is great because we want to set some defaults and apply the <S: Into<String>>
that existed on our original builder implementation, like so:
#[derive(Debug, TypedBuilder)]
struct User {
// Use <S: Into<String>>
#[builder(setter(into))]
name: String,
#[builder(setter(into))]
email: String,
// Sets the default to Guest
#[builder(default = AuthLevel::Guest)]
auth_level: AuthLevel,
// - Use `default` trait (`None` for an `Option`)
// - Allow calling the builder using "address" instead of `Some("address")`
// - Use <S: Into<String>>
#[builder(default, setter(strip_option, into))]
address: Option<String>,
#[builder(default, setter(strip_option, into))]
phone: Option<String>,
}
And we're done!
let alice = User::builder()
.name("Alice")
.email("alice@example.com")
.auth_level(AuthLevel::User)
.phone("+1-321-555-1234")
.build();
If we forget to set one of the fields, such as forgetting .email()
, we'll get a helpful error message instead of the ambiguous "method not found" :
Hopefully these 4 methods have helped to clear up any questions you have on the different ways to instantiate a new structure in Rust.
Each of these techniques have their place in different situations, so go ahead and test them out in your own projects.
And remember. If you struggle with applying these techniques and want to learn more, or simply want a deep dive into learning Rust (regardless of current skill level), then check out my complete Rust course.
You can learn all about the course here, or watch the first few videos for free.
What do you have to lose? Dive in, learn everything about structures in Rust (and more), and then ask questions in the private Discord server!
If you've made it this far, you're clearly interested in Rust so definitely check out all of my Rust posts and content: