Skip to content

Latest commit

 

History

History
188 lines (138 loc) · 6.65 KB

structs.md

File metadata and controls

188 lines (138 loc) · 6.65 KB

Overview

  1. Idiomatic usage of a struct
  2. See traits doc
  3. See modeling doc

Key Concepts

  1. A struct let us group related, named fields into one data structure
    1. Tuples let us group unnamed fields into one data structure
  2. A struct is either fully mutable or fully immutable
  3. Exterior mutability is checked and enforced at compile-time
  4. There are no constructors
    1. Instantiation: doc-1, doc-2
    2. Optionally, you can add a pub fn new(...) -> Self { ... }
  5. Borrowing field data:
    1. Purpose: avoid unnecessary allocation by sharing immutable data
    2. struct needs to outlive fields (easy to specify via lifetimes on struct & borrowed field)

Idioms

  1. Use derive attribute to auto-generate common traits
  2. Ownership: Prefer to own fields in a struct (Why?)
  3. Don't implement to_string(), implement Display
  4. Enforce validation (invariant maintenance). See two options below
  5. Force construction via a function using _priv: (), property

Builder Pattern

Builder: Pros & Cons

  1. Pro: Implementation is trivial (see below)
  2. Pro: Builder has simpler validation pattern
  3. Pro: Builder is simpler than making multiple constructor functions
  4. Pro: Builder allows incremental construction (less local variables)
  5. Con: For small structs, Builder doesn't help much because structs are easily built by field name
  6. More tradeoffs: doc-1, doc-2

Builder: Example

In main.rs or lib.rs

#[macro_use]
extern crate derive_builder;

...

Annotate the struct

#[derive(Builder, Debug)]
// -- Use anyhow::Error as the return type on validation error
// -- Invoke MyStructBuilder::validate for validation
#[builder(build_fn(error = "anyhow::Error", validate = "Self::validate"))]
#[non_exhaustive]  // Force construction via builder or ::new (outside the crate)
pub struct MyStruct {
    pub age: u16,
    pub happy: bool,
    pub weight: f32,

    // -- Allow anything that can convert into ...
    #[builder(setter(into))]
    pub friends: Vec<String>,

    // -- Allow caller to skip setting this field
    #[builder(default)]
    // -- Avoid double wrapping Option
    #[builder(setter(into, strip_option))]
    pub id: Option<i32>,

    // -- Allow anything that can convert into String (eg. &str)
    #[builder(setter(into))]
    pub name: String,

    // -- you can always do:    .host_name("any_string".try_into()?)
    // -- this also allows      .try_host_name("any_string")?
    #[builder(try_setter, setter(into))]
    pub host_name: HostName,

    // -- Don't generate setter for private field
    #[builder(setter(skip))]
    #[builder(default = "17")]
    something_hidden: i64,
}

Usage

  let v1 = MyStructBuilder::default()   // <-- NOTICE: start with default on builder
            .age(123)
            .friends(vec!["foo", "bar"].into_iter().map(String::from).collect_vec())
            .happy(true)
            .name("foo")
            .weight(3.14)
            .build()
            .context("failed to build MyStruct")?;

// NOTE: missing (non-optional) setter throws error: 'X' must be initialized            

Builder: Enforce Validation

impl MyStructBuilder {  // Notice the validate fn is on the Builder struct

    fn validate(&self) -> Result<(), anyhow::Error> {

        // TODO: more validation here

        if let Some(v) = self.age {
            ensure!(v < 90, "too old: {v}");
        }

        Ok(())
    }
}

Enforce validation (without a builder)

  1. Use non_exhaustive attribute to force users (outside the crate) to build via the ::new fn
  2. Only forces validation Has no impact inside the crate, only affects other crates
#[derive(...)]
#[non_exhaustive]  // Force construction via ::new (outside the crate)
pub struct MyStruct {

    pub foo: Foo,
    pub bar: Bar,
    // ... other fields ...
}

impl MyStruct {

    // ::new function is the "constructor" pattern
    pub fn new(
            foo: Foo,
            bar: Bar
            // ... all other fields ...  <-- NOTICE you must pass ALL fields
        ) -> Result<Self, anyhow::Error> {

        let out = Self {
            // ... assign ALL fields here ...
        };

        Self::validate(&out)
            .context("Invalid MyStruct")?;

        Ok(out)
    }

    pub fn validate(data: &Self) -> Result<(), anyhow::Error> {
        // ... run validation here
    }

    ...
}    

Destructuring

  1. TODO

Anti-patterns

  1. Getters
    1. Con: Doesn't help much because field-level (exterior) mutability is already controlled by references & ownership
    2. Pro: Implementation is trivial

TODO/Unorganized

Other Resources

  1. https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/structs.html
  2. https://doc.rust-lang.org/book/ch05-01-defining-structs.html
  3. https://ricardomartins.cc/2016/06/08/interior-mutability
  4. https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/mutability.html#field-level-mutability
  5. https://www.kuniga.me/docs/rust/#struct