Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Counting length of repetition in macro

Tags:

macros

rust

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.

like image 290
user124784 Avatar asked Dec 16 '15 05:12

user124784


3 Answers

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
like image 120
Francis Gagné Avatar answered Oct 27 '22 09:10

Francis Gagné


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.

like image 36
DK. Avatar answered Oct 27 '22 08:10

DK.


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

like image 1
Tarun Aditya Avatar answered Oct 27 '22 10:10

Tarun Aditya