this post was submitted on 16 Jun 2024
28 points (96.7% liked)

Rust

5981 readers
31 users here now

Welcome to the Rust community! This is a place to discuss about the Rust programming language.

Wormhole

!performance@programming.dev

Credits

  • The icon is a modified version of the official rust logo (changing the colors to a gradient and black background)

founded 1 year ago
MODERATORS
 

Hey,

Is there any way to create a macro that allows a Some<T> or T as input?

It's for creating a Span struct that I'm using:

struct Span {
    line: usize,
    column: usize,
    file_path: Option<String>,
}

...and I have the following macro:

macro_rules! span {
    ($line:expr, $column:expr) => {
        Span {
            line: $line,
            column: $column
            file_path: None,
        }
    };

    ($line:expr, $column:expr, $file_path: expr) => {
        Span {
            line: $line,
            column: $column
            file_path: Some($file_path.to_string()),
        }
    };
}

...which allows me to do this:

let foo = span!(1, 1);
let bar = span!(1, 1, "file.txt");

However, sometimes I don't want to pass in the file path directly but through a variable that is Option. To do this, I always have to match the variable:

let file_path = Some("file.txt");

let foo = match file_path {
    Some(file_path) => span!(1, 1, file_path),
    None => span!(1, 1),
}

Is there a way which allows me to directly use span!(1, 1, file_path) where file_path could be "file.txt", Some("file.txt") or None?

Thanks in advance!

all 17 comments
sorted by: hot top controversial new old
[–] Ogeon@programming.dev 18 points 5 months ago (2 children)

Option<T> has a From<T> implementation that lets you write Option::from($file_path).map(|path| path.to_string()) to accept both cases in the same expression.

https://doc.rust-lang.org/std/option/enum.Option.html#impl-From%3CT%3E-for-Option%3CT%3E

[–] silva@sopuli.xyz 2 points 5 months ago (2 children)

This does not work, as rust cannot infer the type of path

[–] BB_C@programming.dev 2 points 5 months ago

A generic impl is impossible.

Imagine you want to turn a Into<String> to Some(val.into()) and Option<Into<String>> to val.map(Into::into).

Now, what if there is a type T where impl From <Option<T>> for String is implemented?
Then we would have a conflict.

If you only need this for &str and String, then you can add a wrapper type OptionStringWrapper(Option<String>) and implement From<T> for OptionStringWrapper for all concrete type cases you want to support, and go from there.

[–] Ogeon@programming.dev 1 points 5 months ago

Right, there may be too many unknowns involved. 🤔

[–] crispy_kilt@feddit.de 15 points 5 months ago

Why not add a new() function that does the same? Macro seems unneccessary

[–] KillTheMule@programming.dev 6 points 5 months ago* (last edited 5 months ago) (1 children)

It's surprisingly simple: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f176852c61dcf0c3382f0ac97c26de03 As a side node, asking for a value, and then immediately calling to_string on it seems kinda hiding the allocation. I'd suggest let the user call to_string on it themselves.

(e) Changed it a bit to account for passing None as the third argument.

[–] Miaou@jlai.lu 2 points 5 months ago (1 children)

I think the point is that the variable itself is an Option. Your example only works for literal Option (although the value inside the optional itself might not be a literal).

One option to OP's problem is to use an auxiliary trait implemented on both string and Option

[–] v9CYKjLeia10dZpz88iU@programming.dev 1 points 5 months ago* (last edited 5 months ago) (1 children)

One option to OP’s problem is to use an auxiliary trait implemented on both string and Option

This looks something like the following

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=aa68fe540dbd09821ecc482423227b46

[–] BB_C@programming.dev 1 points 5 months ago* (last edited 5 months ago) (1 children)

~~* Two of your macro rules are not used 😉 (expand to see which ones).~~

  • This doesn't support Option<&str>. If it did, we would lose literal None support 😉

Similar impl, but using wrapper struct with From impls

[–] v9CYKjLeia10dZpz88iU@programming.dev 1 points 5 months ago* (last edited 5 months ago) (1 children)

Two of your macro rules are not used 😉 (expand to see which ones).

The macro rules are all used. (Macros are matched from top to bottom by the declared match types. The ident/expressions can't match until after the more text based Option matching.)

let _foo = Span { line: 1, column: 1, file_path: None };
let _bar = Span { line: 1, column: 1, file_path: "file.txt".upgrade() };
let _baz = Span { line: 1, column: 1, file_path: Some("file.txt".to_string()) };
let _baz = Span { line: 1, column: 1, file_path: None };
let _baz = Span { line: 1, column: 1, file_path: borrowed.upgrade() };
let _baz = Span { line: 1, column: 1, file_path: owned.upgrade() };

This doesn’t support Option<&str>. If it did, we would lose literal None support 😉

I didn't make Option<&str> an option because the struct is for type Option<String>. It does support Option though.

impl OptionUpgrade for Option<String> {
    fn upgrade(self) -> Option<String> {
        self
    }
}

It looks like the following, and uses the last match case.

let opt: Option<String> = Some("text".into());
let opt = span!(1, 1, opt);

With macro expansion

let opt: Option<String> = Some("text".into());
let opt = Span { line: 1, column: 1, file_path: opt.upgrade() };

There's not anything stopping it from supporting Option<&str> though. This would be the implementation

impl OptionUpgrade for Option<&str> {
    fn upgrade(self) -> Option<String> {
        self.map(|v| v.into())
    }
}
[–] BB_C@programming.dev 1 points 5 months ago (1 children)

The macro rules are all used.

Oops. I was looking at it wrong.

I didn’t make Option<&str> an option because the struct is for type Option<String>.

Re-read the end of OP's requirements.

[–] v9CYKjLeia10dZpz88iU@programming.dev 1 points 5 months ago* (last edited 5 months ago) (1 children)

I made an edit.

There's not anything stopping it from supporting Option<&str> though. This would be the implementation

impl OptionUpgrade for Option<&str> {
    fn upgrade(self) -> Option<String> {
        self.map(|v| v.into())
    }
}

It's also possible to just make it generic over Option types

impl<A: ToString> OptionUpgrade for Option<A> {
    fn upgrade(self) -> Option<String> {
        self.map(|v| v.to_string())
    }
}
[–] BB_C@programming.dev 1 points 5 months ago (1 children)

Yes, but then the concrete type of None literals becomes unknown, which is what I was trying to point out.

[–] v9CYKjLeia10dZpz88iU@programming.dev 1 points 5 months ago* (last edited 5 months ago) (1 children)
[–] v9CYKjLeia10dZpz88iU@programming.dev 1 points 5 months ago* (last edited 5 months ago)

I had commented something similar to stating it was possible to pass the inference problem to the struct if the goal was to support more types, but I removed it because I didn't think my example was easy to understand when reading it later. I made a better example though

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=8651fa6121840658b4b6249399f693c7

[–] tuna@discuss.tchncs.de 1 points 4 months ago

You might be okay with this:

macro_rules! span {
    ($line:expr, $column:expr) => {
        Span {
            line: $line,
            column: $column,
            file_path: None,
        }
    };
    ($line:expr, $column:expr, $file_path:literal) => {
        Span {
            line: $line,
            column: $column,
            file_path: Some($file_path.to_string()),
        }
    };
    ($line:expr, $column:expr, $file_path:expr) => {
        Span {
            line: $line,
            column: $column,
            file_path: $file_path,
        }
    };
}

Playground

However, sometimes I don't want to pass in the file path directly but through a variable that is Option<String>.

Essentially I took this to mean str literals will be auto wrapped in Some, but anything else is expected to be Option<String>