Possible to have a multidimensional array parameter of unknown size?


#1

Is it possible to write a function that takes in a multidimensional array of unknown size? For example:

fn something(grid: &[[char; 7]; 7]) …

But without specifying the sizes?

Cheers,
Brian


#2

In rust, an “Array” is always a fixed size. The canonical way to represent a list of things of unknown size is a Vec<T>. A multidimensional vector could be Vec<Vec<T>>.


#3

I understand that Arrays have a fixed size. I’m wondering if it is possible to write generic functions to process them.


#4

It is possible. You can abstract them as something that can be referenced to as &[T]:

use std::fmt::Display;

fn print_matrix<Matrix: AsRef<[Row]>, Row: AsRef<[T]>, T: Display>(matrix: Matrix) {
    let matrix = matrix.as_ref();
    println!("[");
    
    for row in matrix {
        let row = row.as_ref();
        let mut first = true;
        print!("\t[");
        
        for element in row {
            if !first {
                print!(", {}", element);
            } else {
                print!("{}", element);
                first = false;
            }
        }
        
        println!("; {}]", row.len());
    }
    
    println!("; {}]", matrix.len());
}

fn main() {
    print_matrix([[1; 7]; 7]);
}

The generic type Matrix can be referenced to as a slice of Row and Row can be referenced to as a slice of T. You can replace T with char or whatever else you want. This should also work with Vec<Vec<T>> and any combination of vectors and arrays.


#5

Is this copying the nested array valued argument on the stack by value? Or is the AsRef magic telling the compiler it can pass a reference to the data?


#6

The [v; N] syntax copies the value v N times in an array, so [[1; 7]; 7] results in an array of length 7, containing arrays of length 7, containing a number 1 in each element. Basically an 7 by 7 grid of ones.

AsRef is used to make referencing generic. We know that all arrays implements AsRef<[T]>, so it’s perfect here because we want to make the fixed length generic and a slice allows us to make the type independent of length. The signatures says that we want something that we can take a reference to and we want the type of the reference to be &[T]. We don’t care if it’s an array or anything else, as long as we can get a slice into it, because that’s all we need.

Did that answer your question?


#7

Heh, not really, but I appreciate the effort :smile:

My basic question: Is the argument passed by value? Or is the argument converted to a reference before being passed to the function?

I have some other questions, if you’re willing. Here’s a pared down version of your example:

use std::fmt::Display;
fn hmm<Row: AsRef<[T]>, T: Display>(row: Row) {
    let row = row.as_ref();
    for ele in row {
        print!(" {}", ele);
    }
    print!("\n");
}
 
fn main() {
    let a = [0f64; 7];
    hmm(a);
}

Why isn’t this next signature equivalent?

fn hmm<T: Display>(row: AsRef<[T]>) {

I think I’m not understanding how type substitution works with template arguments. The first says Row implements a trait, the second says row is an instance of the trait? I can make it work if I change to:

fn hmm<T: Display>(row: &AsRef<[T]>) {

and call it with

hmm(&a);

But I don’t understand why the original version doesn’t need to take the reference to a.

This last question is niddly… If I switch the 7 to 77, there isn’t a predefined AsRef implementation (they go up to 32). I tried to add one, but it complained about adding traits to types that weren’t in my crate. So the brief question is: how do you make the next one work? (without switching to a Vec)

fn main() {
    let a = [0f64; 77];
    hmm(a);
}

Thanks in advance for any answers.


#8

Ah, oh, I get it now. Yes, it’s passed by value. Generic type parameters are treated like any type, with the difference that you don’t actually know what they are, so a reference would instead be &Matrix. Rust will generate different variants of the function for different types, so it’s all compile time. I can recommend the part about generics in The Book if you want more details.

Sure :smile: I hope you don’t mind if I keep recommending further reading. You are still welcome to ask questions.

Because AsRef<[T]> is a trait and a trait has no known size at compiletime. You would have to pass it as a pointer of some sort, like &AsRef<[T]> or Box<AsRef<[T]>>. Using a generic type instead is like saying "I don’t care what it is, as long as it implements these traits` and the compiler will generate an appropriate concrete function for us. You can read more about this in the part about trait objects.

That’s a bit more tricky. You can’t, as you observed, implement someone else’s trait for someone else’s type. There are generally two options in such a case: make a wrapper type, like struct Array77<T>([T; 77]);, and implement what you want for it, or make your own conversion trait and use it instead of AsRef. Both of these has their ups and downs, so what you choose depends on what you want to achieve. I think the custom trait would make more sense in this case, since you can make it work for things that implement AsRef<[T]>, as well.

The solution for this particular situation is actually simpler. You can cast a reference to your array to a &[T] instead and pass that to the function. Any reference to an array can be turned into a slice, so we are pretty lucky in this case:

fn main() {
    let a = [0f64; 77];
    hmm(&a as &[f64]);
}

AsRef<T> is always implemented for T, meaning that AsRef<[T]> is implemented for [T], so this works.