8.6 Handling Optional and Named Parameters

Unlike languages such as Python or C++, Rust does not have built-in support for:

  • Default parameter values: Providing a default value if an argument isn’t supplied.
  • Named arguments: Passing arguments using parameter_name = value syntax, allowing arbitrary order.

All function arguments in Rust must be explicitly provided by the caller in the exact order specified in the function signature.

However, Rust offers idiomatic patterns to achieve similar flexibility:

8.6.1 Using Option<T> for Optional Parameters

The standard library type Option<T> (Chapter 14) can represent a value that might be present (Some(value)) or absent (None). This is commonly used to simulate optional parameters.

// 'level' is an optional parameter.
fn log_message(message: &str, level: Option<&str>) {
    // Use unwrap_or to provide a default value if 'level' is None.
    let log_level = level.unwrap_or("INFO");
    println!("[{}] {}", log_level, message);
}

fn main() {
    log_message("User logged in.", None); // Use default level "INFO".
    log_message("Disk space low!", Some("WARN")); // Provide a specific level.
}

8.6.2 The Builder Pattern for Complex Configuration

For functions with multiple configurable parameters, especially optional ones, the Builder Pattern is often used. This involves creating a separate Builder struct that accumulates configuration settings via method calls before finally constructing the desired object or performing the action.

struct WindowConfig {
    title: String,
    width: u32,
    height: u32,
    resizable: bool,
}

// Builder struct
struct WindowBuilder {
    title: String,
    width: Option<u32>,
    height: Option<u32>,
    resizable: Option<bool>,
}

impl WindowBuilder {
    // Start building with a mandatory parameter (title)
    fn new(title: String) -> Self {
        WindowBuilder {
            title,
            width: None,
            height: None,
            resizable: None,
        }
    }

    // Methods to set optional parameters
    fn width(mut self, width: u32) -> Self {
        self.width = Some(width);
        self // Return self to allow chaining
    }

    fn height(mut self, height: u32) -> Self {
        self.height = Some(height);
        self
    }

    fn resizable(mut self, resizable: bool) -> Self {
        self.resizable = Some(resizable);
        self
    }

    // Final build method using defaults for unspecified options
    fn build(self) -> WindowConfig {
        WindowConfig {
            title: self.title,
            width: self.width.unwrap_or(800),   // Default width
            height: self.height.unwrap_or(600), // Default height
            resizable: self.resizable.unwrap_or(true), // Default resizable
        }
    }
}

fn main() {
    let window1 = WindowBuilder::new("My App".to_string()).build(); // Use all defaults

    let window2 = WindowBuilder::new("Editor".to_string())
        .width(1024)
        .height(768)
        .resizable(false)
        .build(); // Specify some options

    println!("Window 1: width={}, height={}, resizable={}",
        window1.width, window1.height, window1.resizable);
    println!("Window 2: width={}, height={}, resizable={}",
        window2.width, window2.height, window2.resizable);
}

The Builder pattern provides clear, readable configuration and handles defaults gracefully, making it a robust alternative to named/default parameters for complex function calls or object construction.