Tired of Writing Some

You wrote a function to set a discount, taking Option<f64>. Most callers just want to pass a number, occasionally passing None to clear the discount.

The result? Every call requires Some(0.15). Code littered with Some and None everywhere. Annoying.

I had the same problem. Then I found a Rust pattern that lets functions accept both raw values and Option.

Rust Into Trait and Option relationship diagram

How It Works

Here’s the code:

fn set_discount(value: impl Into<Option<f64>>) {
    match value.into() {
        Some(v) => println!("Discount set to {}", v),
        None => println!("Discount cleared"),
    }
}

// Callers can write:
set_discount(0.15);      // raw value, auto-converts to Some(0.15)
set_discount(Some(0.15)); // Option works too
set_discount(None);      // None to clear

All three work.

The mechanism is straightforward. Rust’s standard library has impl<T> From<T> for Option<T>, meaning any T can convert to Some(T). And From automatically gives you Into.

So f64 converts to Option<f64>, and Option<f64> is already Option<f64>. Writing impl Into<Option<f64>> in the function signature handles all cases.

Benefits

This Rust trick cleans up call sites. set_discount(0.15) reads better than set_discount(Some(0.15)).

The parameter type itself says “this value is optional” without extra comments.

No performance cost. The compiler optimizes away the wrapping.

Pitfalls

This trick isn’t universal.

impl Trait in argument position is generic. The compiler generates code for each type passed in. Pass f64, get one version. Pass Option<f64>, get another. Fine for internal code, but public libraries should consider binary bloat.

Strings are problematic. &str doesn’t implement Into<Option<String>>, so impl Into<Option<String>> won’t accept &str. Numbers are fine, strings need care.

Type inference occasionally fails. Simple cases like set_discount(None) infer Option<f64>, but complex generics may need explicit type annotations.

Documentation suffers. pub fn foo(value: impl Into<Option<T>>) confuses newcomers. Splitting into set_value() and clear_value() is clearer.

When to Use

This trick works well in internal helper functions with few call sites. Simple types like numbers and bools work great. Teams familiar with Rust won’t have issues.

Avoid it for public library APIs. Avoid it for strings. Avoid it when “no value” and “keep unchanged” need distinction.

Split into two methods for public APIs:

pub struct Config {
    discount: Option<f64>,
}

impl Config {
    pub fn set_discount(&mut self, v: f64) -> &mut Self {
        self.discount = Some(v);
        self
    }

    pub fn clear_discount(&mut self) -> &mut Self {
        self.discount = None;
        self
    }
}

Use Into Trait internally for convenience:

impl Config {
    fn discount_impl(&mut self, v: impl Into<Option<f64>>) -> &mut Self {
        self.discount = v.into();
        self
    }
}

External users get clear docs. Internal code stays convenient.

Handling Strings

To accept &str, String, and None together, use Cow:

use std::borrow::Cow;

fn set_label(label: impl Into<Option<Cow<'static, str>>>) {
    match label.into() {
        Some(text) => println!("Label: {}", text),
        None => println!("No label"),
    }
}

set_label("hello");              // &str
set_label(String::from("hi"));   // String
set_label(None);                 // None

Cow is a “borrowed or owned” type. Saves writing multiple overloads.

Remember

impl Into<Option<T>> is an internal tool, not a public API. Make your team happy, don’t confuse documentation readers.

Code Templates

Minimal one-liner:

fn set_discount(v: impl Into<Option<f64>>) {
    match v.into() {
        Some(x) => println!("Discount: {}", x),
        None => println!("No discount"),
    }
}

Builder pattern, good for public APIs:

#[derive(Default)]
pub struct Request {
    timeout: Option<std::time::Duration>,
}

impl Request {
    pub fn with_timeout(mut self, d: std::time::Duration) -> Self {
        self.timeout = Some(d);
        self
    }

    pub fn without_timeout(mut self) -> Self {
        self.timeout = None;
        self
    }
}

let r1 = Request::default().with_timeout(std::time::Duration::from_secs(3));
let r2 = Request::default().without_timeout();

String version:

use std::borrow::Cow;

fn set_label(label: impl Into<Option<Cow<'static, str>>>) {
    match label.into() {
        Some(s) => println!("Label: {}", s),
        None => println!("No label"),
    }
}

Summary

This Rust tutorial’s impl Into<Option<T>> trick lets functions accept both T and Option<T>, eliminating boilerplate Some. Great for internal use, public APIs should use explicit methods. Bridge strings with Cow.


Questions? Drop a comment or share your own “simple but annoying” Rust experiences.