Search code examples
rustrust-macros

Tuple variable don't match in Rust macro?


Hello i try to implement fill array macro in Rust! And i have a problem!
My macro code:

macro_rules! fill_tensor {
    ($elem:expr; ($dim:expr, $($dims:expr),+)) => {
        vec![fill_tensor!($elem; ($($dims),+)); $dim]
    };
    ($elem:expr; $dim:expr) => {
        vec![$elem; $dim]
    };
}

When i call my macro like this

fill_tensor!(4; (3_usize, 3_usize))

All works correctly (output is 3 by 3 matrix)

But when i call this macro like this

let shape: (usize, usize) = (3_usize, 3_usize)
fill_tensor!(4; shape)

I got error rustc: mismatched typesexpected type usize found tuple (usize, usize)
And my question is Why?

Link To Reproduce problem (Rust Playground)

I i try to see hows $dim looks in this block of code

($elem:expr; $dim:expr) => {
    vec![$elem; $dim]
};              

I make tiny change in code

macro_rules! fill_tensor {
    ($elem:expr; ($dim:expr, $($dims:expr),+)) => {
        vec![fill_tensor!($elem; ($($dims),+)); $dim]
    };
    ($elem:expr; $dim:expr) => {
        assert_eq!($shape, (3_usize, 3_usize)) 
    };
}

And i have no error assertion correct

My final question is Why this pattern $elem:expr; ($dim:expr, $($dims:expr),+) match to (3_usize, 3_usize) but not match to this value in variable? And how to fix it?


Solution

  • Macros are, at their base, text- (more precisely, tokens-) based substitution. They can't see through variables, or inspect their types.

    If you want to support variables, you will have to switch to a type-system-based solution. It may look like the following:

    pub trait FillTensor<Elem> {
        type Output: Clone;
        fn fill_tensor(self, elem: Elem) -> Self::Output;
    }
    
    impl<Elem: Clone> FillTensor<Elem> for usize {
        type Output = Vec<Elem>;
        fn fill_tensor(self, elem: Elem) -> Self::Output {
            vec![elem; self]
        }
    }
    
    impl<Inner: FillTensor<Elem>, Elem: Clone> FillTensor<Elem> for (usize, Inner) {
        type Output = Vec<Inner::Output>;
        fn fill_tensor(self, elem: Elem) -> Self::Output {
            vec![self.1.fill_tensor(elem); self.0]
        }
    }
    

    The usage is like FillTensor::fill_tensor((3, (3, 3)), 4).

    A macro can still make this a tad nicer:

    macro_rules! fill_tensor {
        // A helper to translate `(dim1, dim2, dim3, ...)` to `(dim1, (dim2, (dim3, ...)))`.
        (@construct_dimension ($dim:expr $(, $dims:expr)+ $(,)?)) => {
            ($dim, fill_tensor!(@construct_dimension ($($dims),+)))
        };
        (@construct_dimension $dim:expr) => {
            $dim
        };
        
        ($elem:expr; $($dim:tt)+) => {
            FillTensor::fill_tensor(
                fill_tensor!(@construct_dimension $($dim)+),
                $elem,
            )
        };
    }
    

    The tuple flattening can also be done entirely in the type-system with a bit more complicated trait implementations (this won't allow the custom syntax, though).