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<String>. 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!

      • BB_C@programming.dev
        link
        fedilink
        arrow-up
        2
        ·
        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.

    • Miaou@jlai.lu
      link
      fedilink
      arrow-up
      2
      ·
      5 months ago

      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<string>

          • v9CYKjLeia10dZpz88iU@programming.dev
            link
            fedilink
            arrow-up
            1
            ·
            edit-2
            5 months ago

            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<String> 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
              link
              fedilink
              arrow-up
              1
              ·
              5 months ago

              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
                link
                fedilink
                arrow-up
                1
                ·
                edit-2
                5 months ago

                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
                  link
                  fedilink
                  arrow-up
                  1
                  ·
                  5 months ago

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

  • tuna
    link
    fedilink
    arrow-up
    1
    ·
    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>