4

I'm following the Rust-wasm tutorial and I want to be able to easily add a ship (a shape really) to the Universe in the game of life.

As a first step, I'd like to convert a 2-dimensional array of 0 or 1 representing a shape into a vector of indices representing the coordinates of the shape in the Universe.

I have a working piece of code but I'd like to make it a bit more user-friendly:

const WIDTH: u32 = 64;
const HEIGHT: u32 = 64;

/// glider: [[0, 1, 0], [0, 0, 1], [1, 1, 1]]
fn make_ship(shape: Vec<Vec<u32>>) -> Vec<u32> {
    let mut ship: Vec<u32> = Vec::new();

    for row_idx in 0..shape.len() {
        for col_idx in 0..shape[row_idx].len() {
            let cell = shape[row_idx][col_idx];
            if cell == 1 {
                ship.push(col_idx as u32 + row_idx as u32 * WIDTH);
            }
        }
    }

    ship
}

#[test]
fn glider() {
    let glider  = vec![vec![0, 1, 0], vec![0, 0, 1], vec![1, 1, 1]];
    println!("{:?}", make_ship(glider));
}

The test shows my problem: the verbosity of vec!s. Ideally I'd like to be able to write it without all the vec!. The code of make_ship shouldn't care about the size of the shape arrays. Ideal example:

let glider = [[0, 1, 0],
              [0, 0, 1],
              [1, 1, 1],];

The question is: how to express a shape nicely with simple arrays and have the function make_ship take 2-dimensional vectors of arbitrary size?

ljedrz
  • 20,316
  • 4
  • 69
  • 97
DjebbZ
  • 1,594
  • 1
  • 18
  • 34
  • Not an answer, but just a point: I would avoid if using ‘if’, and just let the loop assign 0s too. Unnecessary branch prediction invocation, and potentially non efficient vectorization, once it is a part of ‘rust’. Modern cpus are really good at moving memory, so I would expect that will be not slower, at least. – Tigran Apr 30 '18 at 12:37
  • @ljedrz thanks for edit, English is not my primary language haha. – DjebbZ Apr 30 '18 at 14:56
  • @Tigran well, I'm just interested in the indices where I should put a live cell, so assigning the 0s would just be noise afterwards for me. – DjebbZ Apr 30 '18 at 14:58

3 Answers3

7

Reducing the number of vec!s is possible with a custom macro:

#[macro_export]
macro_rules! vec2d {
    ($($i:expr),+) => { // handle numbers
        {
            let mut ret = Vec::new();
            $(ret.push($i);)*
            ret
        }
    };

    ([$($arr:tt),+]) => { // handle sets
        {
            let mut ret = Vec::new();
            $(ret.push(vec!($arr));)*
            ret
        }
    };
}

fn main() {
    let glider = vec2d![[0, 1, 0],
                        [0, 0, 1],
                        [1, 1, 1]];

    let glider2 = vec2d![[0, 1, 0, 1],
                         [0, 0, 1, 0],
                         [1, 1, 1, 0],
                         [0, 1, 1, 0]];


    println!("{:?}", glider);  // [[0, 1, 0], [0, 0, 1], [1, 1, 1]]
    println!("{:?}", glider2); // [[0, 1, 0, 1], [0, 0, 1, 0], [1, 1, 1, 0], [0, 1, 1, 0]]
}

Your initial function could also use a bit of an improvement with the help of Rust's iterators:

fn make_ship(shape: Vec<Vec<u32>>) -> Vec<u32> {
    shape
        .iter()
        .enumerate()
        .flat_map(|(row, v)| {
            v.iter().enumerate().filter_map(move |(col, x)| {
                if *x == 1 {
                    Some(col as u32 + row as u32 * WIDTH)
                } else {
                    None
                }
            })
        })
        .collect()
}
ljedrz
  • 20,316
  • 4
  • 69
  • 97
  • I think your macro is exactly what I need: syntactic sugar. Thanks! I'll be learning more macros too. – DjebbZ Apr 30 '18 at 15:01
  • Regarding the second part of your answer, in which regards is your proposal an improvement ? Speed ? Memory usage ? Readability ? – DjebbZ Apr 30 '18 at 15:05
  • 1
    @DjebbZ it's more idiomatic and removes the need for bounds checking (better performance). – ljedrz Apr 30 '18 at 15:26
6

Vec<Vec<_>> is actually not a 2-dimensional vector but a "vector of vectors". This has major implications (assuming the outer vector is interpreted as rows, and the inner as columns):

  1. Rows can have different lengths. That is often not what you would want.
  2. Rows are individual objects, potentially scattered all over the heap memory.
  3. In order to access an element you have to follow two indirections.

I would implement a 2-dimensional vector rather as a 1-dimensional vector with additional information regarding its dimensions. Something like:

struct Vec2D<T> {
    n_rows: usize,  // number of rows
    n_cols: usize,  // number of columns (redundant, since we know the length of data)
    data: Vec<T>,   // data stored in a contiguous 1D array
}

This struct can be initialized with

let glider = Vec2D {
    n_rows: 3,
    n_cols: 3,
    data: vec![0, 1, 0, 
               0, 0, 1, 
               1, 1, 1],
};

Or more conveniently with functions or macros that take arrays-of-arrays. (See @ljedrz's answer for inspiration).

To access an element in the struct you'd have to use a little bit of math to convert a 2D index into a 1D index:

impl<T> Vec2D<T> {
    fn get(&self, row: usize, col: usize) -> &T {
         assert!(row < self.n_rows);
         assert!(col < self.n_cols);
         &self.data[row * self.n_cols + col]
    }
}

While implementing your own 2-dimensional array type is a fun exercise, for productive use it may be more efficient to use an existing solution such as the ndarray crate.

MB-F
  • 22,770
  • 4
  • 61
  • 116
  • 1
    "for productive use it may be more efficient to use an existing solution such as the ndarray crate." and efficiently `get()` is a heavy cost function and ndarray will have a lot of better way to iterate without always do a multiplication. – Stargateur Apr 30 '18 at 14:09
  • @Stargateur yep, absolutely. Nothing prevents from creating our own optimized solutions, though. The question is how desperately one wants to reinvent the wheel :) – MB-F Apr 30 '18 at 14:37
  • Thank you @kazemakase, indeed I could use the ndarray crate (didn't know about it) but as I'm learning I'll without additional tools. – DjebbZ Apr 30 '18 at 15:06
2

Another solution is to transparently handle Vec<T> and [T] using AsRef:

fn make_ship<T>(shape: &[T]) -> Vec<u32>
where
    T: AsRef<[u32]>,
{
    let mut ship: Vec<u32> = Vec::new();

    for row_idx in 0..shape.len() {
        let row = shape[row_idx].as_ref();
        for col_idx in 0..row.len() {
            let cell = row[col_idx];
            if cell == 1 {
                ship.push(col_idx as u32 + row_idx as u32 * WIDTH);
            }
        }
    }

    ship
}

This handles the following:

let glider = vec![vec![0, 1, 0], vec![0, 0, 1], vec![1, 1, 1]];
let glider = [[0, 1, 0], [0, 0, 1], [1, 1, 1]];
let glider = [vec![0, 1, 0], vec![0, 0, 1], vec![1, 1, 1]];
let glider = vec![[0, 1, 0], [0, 0, 1], [1, 1, 1]];

An even better solution is to not care about slices/vectors at all, and use iterators:

fn make_ship<'a, T, U>(shape: &'a T) -> Vec<u32>
where
    &'a T: std::iter::IntoIterator<Item = U>,
    U: std::iter::IntoIterator<Item = &'a u32>,
{
    let mut ship: Vec<u32> = Vec::new();

    for (row_idx, row) in shape.into_iter().enumerate() {
        for (col_idx, &cell) in row.into_iter().enumerate() {
            if cell == 1 {
                ship.push(col_idx as u32 + row_idx as u32 * WIDTH);
            }
        }
    }

    ship
}

Which also handle the cases above, but could also handle a type such as @kazemakase's Vec2D if it provided such iterators.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
mcarton
  • 27,633
  • 5
  • 85
  • 95