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

Rust

6832 readers
3 users here now

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

Wormhole

[email protected]

Credits

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

founded 2 years ago
MODERATORS
28
submitted 10 months ago* (last edited 10 months ago) by [email protected] to c/rust
 

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 19 points 10 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

[–] [email protected] 2 points 10 months ago (2 children)

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

[–] BB_C 2 points 10 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 1 points 10 months ago

Right, there may be too many unknowns involved. 🤔

[–] [email protected] 15 points 10 months ago

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

[–] KillTheMule 6 points 10 months ago* (last edited 10 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.

[–] [email protected] 2 points 10 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 1 points 10 months ago* (last edited 10 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 1 points 10 months ago* (last edited 10 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 1 points 10 months ago* (last edited 10 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 1 points 10 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 1 points 10 months ago* (last edited 10 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 1 points 10 months ago (1 children)

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

[–] [email protected] 1 points 9 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>