9

I'm trying to implement a macro to allow MATLAB-esque matrix creation. I've got a basic working macro but I still have a long way to go.

I want to be able to enforce the right structure (same number of elements in each row) but I'm not sure how to do this within the macro. I think I want to enforce that each internal repetition has the same length - is this something I can do?

Here is my code so far:

pub struct Matrix<T> {
    pub cols: usize,
    pub rows: usize,
    pub data: Vec<T>
}

macro_rules! mat {
    ( $($( $x:expr ),*);* ) => {
        {
            let mut vec = Vec::new();
            let mut rows = 0;

            $(
                $(
                    vec.push($x);
                )*
                rows += 1;
            )*
            Matrix { cols : vec.len()/rows, rows: rows, data: vec}
        }
    };
}

It works but as you can see isn't very safe. It has no restrictions on the structure.

I want to do a lot more with this macro but I think this is a good start!

Update:

Here is some playground code for a crappy implementation I worked out. If anyone has any better suggestions please let me know! Otherwise I'll close this myself.

user124784
  • 896
  • 1
  • 13
  • 22
  • Possible duplicate of [Is it possible to create a macro which counts the number of expanded items?](http://stackoverflow.com/questions/30152800/is-it-possible-to-create-a-macro-which-counts-the-number-of-expanded-items) – ideasman42 Feb 10 '17 at 08:48

3 Answers3

9
macro_rules! count {
    () => (0usize);
    ( $x:tt $($xs:tt)* ) => (1usize + count!($($xs)*));
}

macro_rules! mat {
    ( $( $x:expr ),* ) => { {
        let vec = vec![$($x),*];
        Matrix { cols : vec.len(), rows: 1, data: vec }
    } };
    ( $( $x0:expr ),* ; $($( $x:expr ),*);* ) => { {
        let mut _assert_width0 = [(); count!($($x0)*)];
        let mut vec = Vec::new();
        let rows = 1usize;
        let cols = count!($($x0)*);

        $( vec.push($x0); )*

        $(
            let rows = rows + 1usize;
            let _assert_width = [(); count!($($x)*)];
            _assert_width0 = _assert_width;
            $( vec.push($x); )*
        )*

        Matrix { cols : cols, rows: rows, data: vec }
    } }
}

playground

The count! macro expands to a constant expression that represents the number of arguments it got as input. It's just a helper for the mat! macro. If you need to count a lot of items and the compiler can't cope with it, see the Counting chapter in The Little Book of Rust Macros, which has more complex macros for counting.

My version of the macro uses dummy variables and assignments to verify that the width of all rows are the same. First off, I changed the macro's pattern to handle the first row separately from the subsequent rows. The first variable, _assert_width0, is initialized with an array of units ((), which makes the array take no memory), with the size of the array being the number of items in the first row. Then, _assert_width is also initialized with an array of units, with the size of the array being the number of items in each subsequent row. Then, _assert_width is assigned to _assert_width0. The magic here is that this line will raise a compiler error if the width of a row doesn't match the width of the first row, since the types of the array won't match (you might have e.g. [(); 3] and [(); 4]). The error isn't super clear if you don't know what's going on in the macro, though:

<anon>:38:24: 38:37 error: mismatched types:
 expected `[(); 3]`,
    found `[(); 4]`
(expected an array with a fixed size of 3 elements,
    found one with 4 elements) [E0308]
<anon>:38           _assert_width0 = _assert_width;
                                     ^~~~~~~~~~~~~
<anon>:47:13: 47:44 note: in this expansion of mat! (defined in <anon>)
<anon>:38:24: 38:37 help: see the detailed explanation for E0308
Francis Gagné
  • 60,274
  • 7
  • 180
  • 155
  • Thanks for the response! This is a really smart answer but it does create an issue - the user can't create a single row matrix. Is that something which can be fixed? Can we make the repeated rows optional? – user124784 Dec 17 '15 at 03:49
  • You should replace `0` and `1` in `count!` with `0usize` and `1usize`, or you'll *rapidly* hit exponential compile time problems due to integer inference. Also, this definition of `count!` is only good up to about 64 elements; the link in my answer has more robust definitions. Having said that, using the array length like that is clever; I haven't seen that one before. – DK. Dec 17 '15 at 06:42
  • @user124784: It's just a matter of adding another pattern to the macro, which I've added in my answer (it's much simpler since we don't need to validate the width of rows). – Francis Gagné Dec 17 '15 at 13:04
  • @FrancisGagné That's awesome! I don't think I need to worry about the user exceeding the 64 element count limit. – user124784 Dec 17 '15 at 15:19
5

First, to quickly address the title of your question: see the Counting chapter in The Little Book of Rust Macros. To summarise: there is no direct way, you need to write a macro that expands to something you can count in regular code.

Now, to address your actual question: hoo boy.

It's not so much counting that you want, it's to fail at compile time if the sub-sequences have different lengths.

First of all, there's no clean way to trigger a compilation failure from a macro. You can trigger some other pre-existing error, but you can't control the actual error message.

Secondly, there's no easy way to do "variable" comparisons in macros at all. You can sometimes compare against a fixed token sequence, but you're not doing that here.

So it's doubly not-really-doable.

The simplest thing to do is check the lengths during construction at runtime, and return an error or panic if they don't match.


Is it actually impossible? I don't believe so. If you're willing to accept inscrutable error messages and a massive jump in complexity, you can check for length equality between two token sequences like so:

macro_rules! tts_equal_len {
    (($_lhs:tt $($lhs_tail:tt)*), ($_rhs:tt $($rhs_tail:tt)*)) => {
        tts_equal_len!(($($lhs_tail)*), ($($rhs_tail)*))
    };
    (($($_lhs_tail:tt)+), ()) => { do_something_bad!() };
    ((), ($($_rhs_tail:tt)+)) => { do_something_bad!() };
    ((), ()) => { do_something_good!() };
}

macro_rules! do_something_bad { () => { { println!("kaboom!") } } }
macro_rules! do_something_good { () => { { println!("no kaboom!") } } }

fn main() {
    tts_equal_len!((,,,), (,,,));
    tts_equal_len!((,,,), (,,));
    tts_equal_len!((,), (,,));
}

Again, the real problem is finding some way to fail at compile time such that the user will understand why compilation failed.

DK.
  • 55,277
  • 5
  • 189
  • 162
  • I was worried this might be the case. Thanks for your implementation - I have no idea how it works but I'll figure it out! Maybe it's easier in this case to write a compiler plugin? I haven't looked at them yet but from what I've read this is a decent use case? – user124784 Dec 16 '15 at 06:01
  • @user124784: You could do that, but keep in mind that compiler plugins are unstable, break frequently, and cannot be used on anything other than nightly compiler builds. – DK. Dec 16 '15 at 10:28
  • As of Rust 1.20 (August 2017) the [`compile_error!`](https://doc.rust-lang.org/std/macro.compile_error.html) macro is available which allows you to fail at compile time with a user-readable error message. This makes the `tts_equal_len` macro proposed here much more readable for the end user and avoids needing to insert intentionally bad code to fail compilation when the tts aren't the same. – zstewart Apr 12 '23 at 15:35
1

Update: there's a new way of doing things

As of the day on which this was written, the feature of rust which enables the following (count) to be done, in still unstable and is available in nightly builds.
You can check out the github issues and test cases for further understanding of what's given below

To enable this feature, you need to add the line #![feature(macro_metavar_expr)] to the top of the crate's root module (usually main.rs or lib.rs), and also set your repo to use nightly builds, which is easily done by creating a file rust-toolchain.toml in the root directory (alongside Cargo.toml) and add the folllowing lines to it:

[toolchain]
channel = "nightly"

Now, instead of providing a solution to you specific problem, I'd like to share a generic solution I created to better illustrate most situations.
I highly recommend studying the code AND the comments, by pasting the following two code blocks in a file (main.rs).

The macro_rules


#[derive(Eq, PartialEq, Debug, Copy, Clone)]
struct SumLen {
    sum: i32,
    len: u32
}

/// currently one `i32` type is available
///
/// # Examples
///
/// The output of the following:
/// ```ignore
/// sumnarr!(i32 => 5 ; 6, 7, 8)
/// ```
/// will be `[(5, 1), (21, 3)]`
macro_rules! sumnarr {
    ( $type:ty => $( $( $x: expr ),* );* ) => {
        {
            // `${count(x,0)}` will give you "length" (number of iterations)
            // in `$( )*` loop that you are IMMEDIATELY OUTSIDE OF (e.g.: the `$( )*` loop below)

            // `${count(x,1)}` will give you TOTAL number of iterations that the `$( )*` loop
            // INSIDE of the IMMEDIATE `$( )*` loop will make. i.e. it is similar to converting
            // [ [i1,i2], [i1,i2,i3] ] TO [ i1,i2,i3,i4,i5 ] i.e. flatten the nested iteration.

            // In case of `[ [i1,i2], [i1,i2,i3] ]`, `${count(x,0)}` is 2 and `${count(x,1)}` is 5

            let mut arr: [SumLen; ${count(x,0)}] = [SumLen{ sum:0, len:0}; ${count(x,0)}];
            $(
                // `${index()}` refers to the iteration number within the `$( )*` loop
                arr[${index()}] = {

                    let mut sum = 0;
                    //let mut len = 0;

                    // THe following will give us length is the loop it is IMMEDIATELY OUTSIDE OF
                    // (the one just below)
                    let len = ${count(x,0)};

                    $(
                        sum += $x;

                        // If you were NOT using `$x` somewhere else inside `$(  )*`,
                        // then you should use `${ignore(x)};` to inform the compiler

                        //You could use the below method, where `${length()}` will give you
                        //"length" or "number of iterations" in current loop that you are in
                        // OR
                        // you could go with my method of `${count(x,0)}` which is explained above

                        //len = ${length()};
                    )*
                    SumLen {
                        sum,
                        len
                    }
                };
            )*
            arr
        }
    };
}

The #[test] (unit test)


#[test]
fn sumnarr_macro() {
    let (a, b, c, d, e) = (4, 5, 6, 9, 10);
    let sum_lens = [
        SumLen {
            sum: a + e,
            len: 2
        },
        SumLen {
            sum: b + c + d,
            len: 3
        }
    ];
    assert_eq!(sum_lens, sumnarr!(i32 => a,e;b,c,d));
}

I hope this helps

Tarun Aditya
  • 41
  • 1
  • 5