Functional Options in Go and Rust
By Andreas Røssland
The Functional Options Pattern is often used to create objects in Go. It’s a good middle ground between the simplicity of struct literals and the safety/usability of the Builder Pattern. But how do I implement it in Rust? These are my notes as a professional Gopher and a novice Crustacean.
Disclaimer: The Rust code below was intentionally written without researching Rust idioms or patterns.
Creating objects in Go
Just using struct literals is a great way to construct objects in Go. It’s only when you need additional safety and the number of arguments is getting unwieldy that you would need something more complex.
type Person struct {
FirstName string
LastName string
}
// Usage:
person := &Person{FirstName: "John", LastName: "Doe"}
For example, if you need to validate the struct fields, or the fields are private. The next level of safety and complexity is a constructor. Like in Rust, constructors in Go are just ordinary functions that return your type, or a pointer to your type. They are conventionally named NewSomething
.
package person
type Person struct {
firstName string
lastName string
}
func New(firstName, lastName string) (*Person, error) {
person := &Person{firstName, lastName}
if err := validatePerson(person); err != nil {
return nil, err
}
return person, nil
}
// Usage::
package person_test
p, err := person.New("John", "Doe")
A constructor like this is usually good enough, but there are arguments for using a more complex pattern: Having a large constructor can be error-prone if types are similar and there are many arguments, as some of them can be switched around without any compiler warnings. Adding additional arguments will also require all usages to be modified.
// These are equally valid. Compiler doesn't catch this.
person.New(input.firstName, input.lastName)
person.New(input.lastName, input.firstName)
// It's not possible to combine arguments
person.New(myDefaultPerson)
To justify a more complex pattern I will use a slightly more complex type.
package monitor
type Resolution struct {
Width int
Height int
}
type Monitor struct {
tech string
resolution Resolution
hasStand bool
}
The use case here is to always be certain that a non-nil Monitor is valid whenever used outside the package. Because of this, all fields must be private to ensure other packages cannot mutate it. A simple constructor would be approaching too large, in my opinion.
mon, err := monitor.NewSimple(
monitor.Resolution{Width: 1920, Weight: 1080},
true,
"OLED",
)
Functional Options Pattern in Go
Functional Options use variadic functions and first-class functions to provide a more fluent-ish interface for creating objects. It’s a bit verbose, but also self-documenting without IDE hints.
package monitor_test
mon, err := monitor.New(
monitor.WithTech("OLED"),
monitor.WithResolution(1920, 1080),
monitor.WithStand,
)
The New
constructor loops over a list of mutator functions.
package monitor
func New(options ...func(*Monitor)) (*Monitor, error) {
m := Monitor{}
for _, option := range options {
option(&m)
}
if err := validate(&m); err != nil {
return nil, err
}
return &m, nil
}
func validate(m *Monitor) error {
if m.tech == "" {
return fmt.Errorf("tech is required")
}
// ... more validation here
return nil
}
The options functions are defined as follows. Note that they return a mutator function.
func WithTech(tech string) func(*Monitor) {
return func(monitor *Monitor) {
monitor.tech = tech
}
}
func WithResolution(width, height int) func(*Monitor) {
return func(monitor *Monitor) {
monitor.resolution = Resolution{width, height}
}
}
Options without arguments can be simplified, and simply be mutators instead of returning mutators. I usually don’t do this as the interface becomes less consistent.
var WithoutStand = func(monitor *Monitor) {
monitor.hasStand = false
}
Creating objects in Rust
Warning: Here be non-idiomatic beginner level Rust, and inaccurate explanations of Rust concepts.
Struct literals
Simple, clean and easy. However, it lacks validation, and fields must be public.
mod monitor {
#[derive(Debug)]
pub struct Resolution {
pub width: u16,
pub height: u16,
}
#[derive(Debug)]
pub struct Monitor {
pub tech: String,
pub resolution: Resolution,
pub has_stand: bool,
}
}
fn main() {
let mon = monitor::Monitor {
tech: String::from("OLED"),
resolution: monitor::Resolution {
width: 1920,
height: 1080,
},
has_stand: true,
};
println!("{:?}", mon)
}
Constructor
mod monitor {
// ...
#[derive(Debug)]
pub struct Monitor {
tech: String, // no longer pub
resolution: Resolution,
has_stand: bool,
}
pub fn new(tech: String, resolution: Resolution, has_stand: bool) -> Result<Monitor, String> {
let mon = Monitor {
tech,
resolution,
has_stand,
};
if let Err(e) = validate(&mon) {
return Err(e);
}
Ok(mon)
}
fn validate(monitor: &Monitor) -> Result<(), String> {
if monitor.tech == "" {
return Err(String::from("tech field is empty"));
} // ... more validations here
Ok(())
}
}
fn main() {
let mon = monitor::new(
String::from("OLED"),
monitor::Resolution { width: 1920, height: 1080 },
true,
);
println!("{:?}", mon);
}
Functional Options in Rust
Let’s implement the options pattern in Rust. I started with the following skeleton:
#![allow(dead_code, unused_variables)]
mod monitor {
// ...
pub type OptionFn = todo!();
// has to be Vec since length cannot be determined
// at compile time.
pub fn new(options: Vec<OptionFn>) -> Monitor {
let mut monitor = Monitor { ... };
for option_fn in options {
option_fn(&mut monitor);
}
monitor
}
pub fn with_stand = todo!();
}
fn main() {
let mut mon = monitor::Monitor {
tech: String::from("OLED"),
resolution: monitor::Resolution {
width: 0,
height: 0,
},
has_stand: false,
};
monitor::with_stand(&mut mon);
println!("{:?}", mon);
}
with_stand
Starting simple, I first made a with_stand
function that mutates an existing object.
pub fn with_stand(monitor: & mut Monitor) -> () {
monitor.has_stand = true;
}
let mut mon = ...
monitor::with_stand(&mut mon);
This works fine, as shown by the output:
Monitor { tech: "OLED", resolution: Resolution { width: 0, height: 0 }, has_stand: true }
new
First attempt at a constructor that takes a list of options:
pub type OptionFn = fn(&mut Monitor) -> ();
pub fn new(options: Vec<OptionFn>) -> Monitor {
let mut monitor = Monitor {
resolution: Resolution {
width: 0,
height: 0,
},
has_stand: false,
tech: String::from("OLED"),
};
for option_fn in options {
option_fn(&monitor);
}
monitor
}
Rust has multiple function types, and only some of them can hold closures.
- Primitive Type
fn
. Function pointer. Simplest, cannot hold a closure. - Trait:
Fn
. Likefn
, but can also hold closures which “only take immutable references to captured variables”. Can containfn
. Can be used instead ofFnMut
andFnOnce
. - Trait:
FnMut
. LikeFn
, but can take mutable references to captured variables. Meaning that calling the function could mutate the captured variables. This again means that calling the function is a mutation, so a caller is only allowed to call the function if it is owned or mutably borrowed. - Trait:
FnOnce
. LikeFn
andFnMut
, but can only be called once.Fn
orFnMut
can be used instead ofFnOnce
. It could mutate captured variables, but it doesn’t matter since nobody is allowed to access them afterwards.
This seems to work just fine:
let mon = monitor::new(vec![
monitor::with_stand,
]);
println!("{:?}", mon);
// Monitor { ..., has_stand: true }
with_resolution
First attempt:
pub fn with_resolution(w: u16, h: u16) -> OptionFn {
|monitor: &mut Monitor| {
monitor.resolution = Resolution { width: w, height: h };
}
}
let mon = monitor::new(vec![
monitor::with_stand,
monitor::with_resolution(1920, 1080),
]);
Compiler error, as expected:
error[E0308]: mismatched types
--> src/main.rs:41:9
|
40 | pub fn with_resolution(w: u16, h: u16) -> OptionFn {
| -------- expected `for<'a> fn(&'a mut Monitor)` because of return type
41 | / |monitor: &mut Monitor| {
42 | | monitor.resolution = Resolution { width: w, height: h };
43 | | }
| |_________^ expected fn pointer, found closure
|
= note: expected fn pointer `for<'a> fn(&'a mut Monitor)`
found closure `[closure@src/main.rs:41:9: 41:32]`
note: closures can only be coerced to `fn` types if they do not capture any variables
--> src/main.rs:42:54
|
42 | monitor.resolution = Resolution { width: w, height: h };
| ^ ^ `h` captured here
| |
| `w` captured here
What does this even mean?
First, with_resolution
no longer returns a simple function pointer. It is now a closure, since it captures the variables w
and h
. These aren’t static, since they are different for different instances of the function, and cannot be determined at compile time. with_resolution(1920, 1080)
has captured (1920, 1080), while with_resolution(640, 480)
has captured (640, 480). Clearly both of the returned functions cannot refer to the same data. Think of a closure like a primitive function combined with a record of captured variables. With this in mind, I understand what this means: closures can only be coerced to
fn types if they do not capture any variables
.
I’m still having a hard time understanding the lifetime syntax for<'a> fn(&'a mut Monitor)
but I’ll ignore that for now, since I will need closures anyways, and that type seems to be for primitive function pointers.
Second attempt:
pub type OptionFn = Fn(&mut Monitor) -> ();
The compiler now complains that “trait objects must include the dyn
keyword”, and suggests adding dyn
in front of Fn
. So what’s a dyn
?
- Keyword
dyn
. It is used to highlight that calls to methods on the associated trait are dynamically dispatched. - Dynamic dispatch. Which implementation of a polymorphic method or function to call is determined at run time. This is commonly used in object-oriented programming where similar objects can both implement an interface. For example, a
Duck
’stalk
method could outputquack quack
and aDog
’stalk
method could outputwoof woof
. But if either are assigned to a variable of typeAnimal
we no longer know which method to call during compile time. We have to do a “dynamic dispatch”, meaning that we look up the object’s type, and call the correcttalk
method. - Traits. Traits are interfaces and define a behaviour an object must implement. If any type implements
talk()->()
we could say that it “implements theTalk
trait”. - Trait objects. “Trait objects point to both an instance of a type implementing a trait, and a table used to look up traits method on that type at runtime.”.
It seems that once the return type is a trait and no longer a concrete type, we need the same type-to-function mapping as in the OOP-example above.
Third attempt:
pub type OptionFn = dyn Fn(&mut Monitor) -> ();
Compiler error:
error[E0277]: the size for values of type `(dyn for<'a> Fn(&'a mut Monitor) + 'static)` cannot be known at compilation time
--> src/main.rs:20:25
|
20 | pub fn new(options: Vec<OptionFn>) -> Monitor {
| ^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `(dyn for<'a> Fn(&'a mut Monitor) + 'static)`
note: required by a bound in `Vec`
--> /Users/aros/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:396:16
|
396 | pub struct Vec<T, #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global> {
| ^ required by this bound in `Vec`
The problem this time seems to be that after allowing OptionFn
s to hold closures, items no longer have a static size, and Vec
requires each item to have a size that can be determined at compilation time. Some quick googling recommends wrapping dyn Fn(&mut Monitor) -> ()
in a Box
.
- Box. A pointer type that uniquely owns a heap allocation of type
T
. Used when size isn’t known at compile time, when you want to transfer ownership of heap data, and “When you want to own a value and you care only that it’s a type that implements a particular trait rather than being of a specific type”. This seems to be closely related to Trait Objects.
Fourth attempt. I had to convert with_stand
so that it returns a boxed primitive function pointer (fn
implements Fn
).
pub type OptionFn = Box<dyn Fn(&mut Monitor) -> ()>;
pub fn with_stand() -> OptionFn {
Box::new(|monitor: &mut Monitor| {
monitor.has_stand = true;
})
}
pub fn with_resolution(w: u16, h: u16) -> OptionFn {
Box::new(|monitor: &mut Monitor| {
monitor.resolution = Resolution { width: w, height: h };
})
}
Progress is a new compiler error.
error(E0373): closure may outlive the current function, but it borrows w
, which is owned by the current function
--> src/main.rs:43:18
|
43 | Box::new(|monitor: &mut Monitor| {
| ^^^^^^^^^^^^^^^^^^^^^^^ may outlive borrowed value `w`
44 | monitor.resolution = Resolution { width: w, height: h };
| - `w` is borrowed here
|
note: closure is returned here
--> src/main.rs:43:9
|
43 | / Box::new(|monitor: &mut Monitor| {
44 | | monitor.resolution = Resolution { width: w, height: h };
45 | | })
| |______^
help: to force the closure to take ownership of `w` (and any other referenced variables), use the `move` keyword
|
43 | Box::new(move |monitor: &mut Monitor| {
| ++++
What does it mean that the “closure may outlive the current function”? When we called with_resolution(w, h)
, ownership of w
and h
was transferred to with_resolution
. They are stored on the stack and will be destroyed when the function returns. That is a problem since the returned closure still depend on them. The solution is to transfer the ownership of the variables to the closure before returning. As suggested, this is done using move
.
Fifth attempt: Success
Adding move
did the trick. The implementation so far:
mod monitor {
#[derive(Debug)]
pub struct Resolution {
pub width: u16,
pub height: u16,
}
#[derive(Debug)]
pub struct Monitor {
tech: String,
resolution: Resolution,
has_stand: bool,
}
pub type OptionFn = Box<dyn Fn(&mut Monitor)>;
pub fn new(options: Vec<OptionFn>) -> Monitor {
let mut monitor = Monitor {
resolution: Resolution {
width: 0,
height: 0,
},
has_stand: false,
tech: String::from(""),
};
for option_fn in options {
option_fn(&mut monitor);
}
monitor
}
pub fn with_stand() -> OptionFn {
Box::new(|monitor: &mut Monitor| {
monitor.has_stand = true;
})
}
pub fn with_resolution(w: u16, h: u16) -> OptionFn {
Box::new(move |monitor: &mut Monitor| {
monitor.resolution = Resolution { width: w, height: h };
})
}
}
fn main() {
let mon = monitor::new(vec![
monitor::with_stand(),
monitor::with_resolution(1920, 1080),
]);
println!("{:?}", mon);
}
with_tech
There is also some Rust trickery here. Especially concerning &str
vs String
.
pub fn with_tech(tech: &str) -> OptionFn {
let tech = tech.to_string();
Box::new(move |monitor: &mut Monitor| {
monitor.tech = tech;
})
}
let mon = monitor::new(vec![
...
monitor::with_tech("OLED")),
]);
String literals, for example “OLED” are stored in a special read-only section of the compiled executable. with_tech
takes a pointer to that string. It has a static
lifetime (i.e. lives for the entire duration of the program).
to_string
is used to get ownership of the input string.
Conclusion
The options pattern seems completely viable in Rust, and my fight with the borrow checker forced me to learn more about how closures are implemented.
If you have any feedback or corrections, feel free to comment or shoot me an email.