Trying to understanding Polymorphism in Rust with a dotproduct example

Hi,
I'm a C and Fortran programmer for high performance systems and want to dive into Rust. I read "The Rust Programming Language", but I learn new languages mostly by playing around with the compiler and trying out stuff. I would like to understand how polymorphism works in Rust.
My aim is to have something like this and the compiler working out the appropriate types:

let vector = Vector::new(5);
let matrix = Matrix::new(5,5);
let result = dotproduct(vector, matrix);

The dotproduct function should result in a new vector. If I give it two vectors it should produce a new vector and if I give it two matrices it should return a matrix.
In Fortran I would implement the appropriate function for each type combination and create an interface and the compiler will figure out what actual function to call.
In C this is not really possible.

Here is what I got so far:

struct Vector {
    len: usize,
    data: Vec<f64>
}

impl Vector {
    pub fn new(len: usize) -> Vector {
        return Vector {
            len: len,
            data: vec![0.0; len]
        }
    }   
    pub fn print(&self) {
        print!("Vector (len = {}):", self.len);
        for idx in 0..self.len {
            print!(" {:5.3}", self.data[idx]);
        }
        print!("\n");
    }   
}

struct Matrix {
    nrows: usize,
    ncols: usize,
    data: Vec<f64>
}

impl Matrix {
    pub fn new(nrows: usize, ncols: usize) -> Matrix {
        return Matrix {
            nrows: nrows,
            ncols: ncols,
            data: vec![0.0; nrows*ncols]
        }
    }   
    pub fn print(&self) {
        print!("Matrix ({}X{}):\n", self.nrows, self.ncols);
        for irow in 0..self.nrows {
            let rowidx = irow*self.ncols;
            for icol in 0..self.ncols {
                let idx = rowidx + icol;
                print!(" {:5.3}", self.data[idx]);
            }
            print!("\n");
        }
    }   
}

// This is the part where I am unsure what to do exactly
// because I do not understand the trait and generics system fully
trait DotProduct {
    fn dotproduct<A,B>(a: &A, b: &B) -> B;
}

impl DotProduct for Vector {
    fn dotproduct<Matrix, Vector>(a: &Matrix, b: &Vector) -> Vector {
        let mut result = Vector::new(a.nrows);
        // Some calculation on `a` and `b` to be done!
        return result;
    }   
}

fn main() {
    let vector = Vector::new(5);
    vector.print();
    let matrix = Matrix::new(5,5);
    matrix.print();

    let result = dotproduct(matrix, vector);
}

I am defining structs to hold the data for a vector and a matrix, and define new and print methods for those.
Then my understanding of Rust is not enough to continue.
I understand that I need a trait lets call it DotProduct where I specify which functionality needs to be implemented if a struct should adopt the trait. Since the function I define is not a method but an associated function with different types, I thought type parameters are the way to go.
In the trait implementation block I thought I should fill in the types I want to implement the function for.
If I try to compile it I get the following error messages:

$ cargo run
   Compiling testcase v0.1.0 (/home/fuhl/software/Rust/testcase)
error[E0425]: cannot find function `dotproduct` in this scope
  --> src/main.rs:69:18
   |
69 |     let result = dotproduct(matrix, vector);
   |                  ^^^^^^^^^^ not found in this scope

error[E0599]: no function or associated item named `new` found for type parameter `Vector` in the current scope
  --> src/main.rs:57:34
   |
57 |         let mut result = Vector::new(a.nrows);
   |                                  ^^^ function or associated item not found in `Vector`

error[E0609]: no field `nrows` on type `&Matrix`
  --> src/main.rs:57:40
   |
56 |     fn dotproduct<Matrix, Vector>(a: &Matrix, b: &Vector) -> Vector {
   |                   ------ type parameter 'Matrix' declared here
57 |         let mut result = Vector::new(a.nrows);
   |                                        ^^^^^

Some errors have detailed explanations: E0425, E0599, E0609.
For more information about an error, try `rustc --explain E0425`.
error: could not compile `testcase` due to 3 previous errors

The first thing I do not understand is why new is not found on line 57. I clearly implemented new for the Vector struct.
Next I do not understand why the nrows is apparently not a field in the Matrix struct on line 57. I clearly defined it to be.

Those errors do not appear if I access the fields or methods outside of the impl scope for the DotProduct trait.
If I take out the trait related stuff, the code compiles and runs fine.

Could someone also explain to me how to achieve the kind of polymorphism I am aiming at here?

Thanks a lot in advance. Help is appreciated.

Your problem on line 57 is that you're referring to a type parameter named Vector and not your Vector type. You can't fill in a type parameter with an actual type when you're defining a function like that. This is also the source of the error about the field not being found.

When you want a type to be specified by the trait impl, you can use an associated type instead of a generic function. Associated types allow you to have a single type that is always used by each impl. This is in contrast to a function with type parameters, where the caller can pick whatever type they want to fill in those parameters[1].

You also need a way to specify different impls for multiplying a matrix by either a matrix or a vector. That's typically done with a type parameter on the trait. A type parameter at the trait level applies to the whole impl instead of a single function, which means we can provide different implementations of the trait for multiplying by a vector and a matrix.

Incidentally those two changes will result in a DotProduct trait that looks very similar to the standard multiplication trait std::ops::Mul

You are also trying to call dotproduct as a free function, but it's attached to a type in your original trait. It makes more sense to have it as a method that takes a self rather than an associated function, but you could leave it if you wanted and call it as DotProduct::dotproduct(&a, &b).

I went for just making it a method though.

Playground

struct Vector {
    len: usize,
    data: Vec<f64>,
}

impl Vector {
    pub fn new(len: usize) -> Vector {
        return Vector {
            len: len,
            data: vec![0.0; len],
        };
    }
    pub fn print(&self) {
        print!("Vector (len = {}):", self.len);
        for idx in 0..self.len {
            print!(" {:5.3}", self.data[idx]);
        }
        print!("\n");
    }
}

struct Matrix {
    nrows: usize,
    ncols: usize,
    data: Vec<f64>,
}

impl Matrix {
    pub fn new(nrows: usize, ncols: usize) -> Matrix {
        return Matrix {
            nrows: nrows,
            ncols: ncols,
            data: vec![0.0; nrows * ncols],
        };
    }
    pub fn print(&self) {
        print!("Matrix ({}X{}):\n", self.nrows, self.ncols);
        for irow in 0..self.nrows {
            let rowidx = irow * self.ncols;
            for icol in 0..self.ncols {
                let idx = rowidx + icol;
                print!(" {:5.3}", self.data[idx]);
            }
            print!("\n");
        }
    }
}

trait DotProduct<Other> {
    type Output;
    fn dotproduct(&self, other: &Other) -> Self::Output;
}

// Now we can create separate impls for Vectors and Matrices on each type.
impl DotProduct<Vector> for Matrix {
    type Output = Vector;

    fn dotproduct(&self, other: &Vector) -> Self::Output {
        let mut result = Vector::new(self.nrows);
        // Some calculation on `a` and `b` to be done!
        return result;
    }
}

impl DotProduct<Matrix> for Matrix {
    type Output = Matrix;

    fn dotproduct(&self, other: &Matrix) -> Self::Output {
        Matrix::new(self.nrows, self.ncols)
    }
}

fn main() {
    let vector = Vector::new(5);
    vector.print();
    let matrix = Matrix::new(5, 5);
    matrix.print();

    let result = matrix.dotproduct(&vector);

    let result = matrix.dotproduct(&matrix);
}

  1. assuming the type satisfies any bounds on the function of course ↩ī¸Ž

5 Likes

Thank you so much for the very quick reply and the great explanation.
I tried to adapt your code so it does not produce methods, but stand alone functions and I think I got it to work:

trait DotProduct<B,C> {
    fn dotproduct(&self, b: &B) -> C;
}

impl DotProduct<Vector, Vector> for Matrix {
    fn dotproduct(&self, b: &Vector) -> Vector {
        let mut result = Vector::new(b.len);
        return result;
    }   
}

impl DotProduct<Matrix, Matrix> for Matrix {
    fn dotproduct(&self, b: &Matrix) -> Matrix {
        let mut result = Matrix::new(5,5);
        return result;
    }   
}
...
let result = DotProduct::dotproduct(&matrix, &vector);
    result.print();
    let result2 = DotProduct::dotproduct(&matrix, &matrix);
    result2.print();

My previous mistake with line 57 was that I thought I had to supply the Types for which I want to do the implementation in place of the type parameters of the dotproduct function. Thanks for clarifying that for me. I think I got a better understanding of it now.

Those are still methods (they have a self parameter) you're just calling them with associated function syntax

Methods can be called either way, but functions without a self can't be called with method call syntax.

Interesting.
I tried to get it to work without a self parameter like this:

trait DotProduct<A,B,C> {
    fn dotproduct(a: &A, b: &B) -> C;
}

impl DotProduct<Matrix, Vector, Vector> {
    fn dotproduct(a: Matrix, b: &Vector) -> Vector {
        let mut result = Vector::new(b.len);
        return result;
    }   
}

But the compiler complained and suggested I declare it impl dyn DotProduct .... However, this lead to more problems:

cargo run
   Compiling testcase v0.1.0 (/home/fuhl/software/Rust/testcase)
error[E0038]: the trait `DotProduct` cannot be made into an object
  --> src/main.rs:55:6
   |
55 | impl dyn DotProduct<Matrix, Vector, Vector> {
   |      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `DotProduct` cannot be made into an object
   |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> src/main.rs:52:8
   |
51 | trait DotProduct<A,B,C> {
   |       ---------- this trait cannot be made into an object...
52 |     fn dotproduct(a: &A, b: &B) -> C;
   |        ^^^^^^^^^^ ...because associated function `dotproduct` has no `self` parameter
help: consider turning `dotproduct` into a method by giving it a `&self` argument
   |
52 |     fn dotproduct(&self, a: &A, b: &B) -> C;
   |                   ++++++
help: alternatively, consider constraining `dotproduct` so it does not apply to trait objects
   |
52 |     fn dotproduct(a: &A, b: &B) -> C where Self: Sized;
   |                                      +++++++++++++++++

From this compiler error I assumed, that it was not possible to make something without a self parameter, which annoyed me a bit.

Could you tell me how I would construct this as associated functions without a self parameter?
Thank you for your patience and help. It is highly appreciated.

Adding a type parameter to the trait doesn't really make sense. You want the type implementing the trait to be the first parameter to the function still, otherwise you're going to run into ambiguous type errors.

Replacing the associated type with another type parameter may cause problems in some generic contexts as well. Semantically an associated type is the correct choice because the caller can't pick the type, it's a property of the impl.

fn whatever(&self) {...} is sugar for fn whatever(self: &Self) {...} where Self means "the type implementing the trait". So we can just replace &self with a: &Self to make dotproduct not a method.

trait DotProduct<B> {
    type Output;
    fn dotproduct(&self, b: &B) -> Self::Output;
}

impl DotProduct<Vector> for Matrix {
    type Output = Vector;

    fn dotproduct(&self, b: &Vector) -> Vector {
        let mut result = Vector::new(b.len);
        return result;
    }
}

impl DotProduct<Matrix> for Matrix {
    type Output = Matrix;
    fn dotproduct(&self, b: &Matrix) -> Matrix {
        let mut result = Matrix::new(5, 5);
        return result;
    }
}

That being said, I think it would be most idiomatic for it to be a method. There's not really any reason for it not to be a method. If you just really want to force it to be called with the dotproduct(A, B) syntax for notational consistency or something, you can just keep the methods in the trait definition and define a function that uses the trait.

trait DotProduct<B> {
    type Output;
    fn dotproduct(&self, b: &B) -> Self::Output;
}

impl DotProduct<Vector> for Matrix {
    type Output = Vector;

    fn dotproduct(&self, b: &Vector) -> Vector {
        let mut result = Vector::new(b.len);
        return result;
    }
}

impl DotProduct<Matrix> for Matrix {
    type Output = Matrix;
    fn dotproduct(&self, b: &Matrix) -> Matrix {
        let mut result = Matrix::new(5, 5);
        return result;
    }
}

fn dotproduct<A, B>(a: &A, b: &B) -> A::Output
where
    A: DotProduct<B>,
{
    a.dotproduct(b)
}

fn main() {
    let matrix = Matrix::new(1, 1);
    let vector = Vector::new(1);
    let result = dotproduct(&matrix, &vector);
    result.print();
    let result2 = dotproduct(&matrix, &matrix);
    result2.print();
}
1 Like

Thank you so so much for the insight into all this, and the detailed explanations.
There are still some minor details that I have to think about for a while, but you already helped me a great deal.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.