Mismatched types when using associated consts and const generics in Rust nightly

687 Views Asked by At

So for a library I am writing, I want to calculate the distance between two points in N dimensions (2, 3, 4, etc...) and I have a Point trait so that user's of the library can use this function on their own types, so long as they are "point like".

I have a trait, "Point", which has the dimensions (N) and floating point type (T) kept as an associated type and constant:

pub trait Point: Copy + Clone {
    type T: num::Float;
    const N: usize;

    fn as_array(&self) -> [Self::T; Self::N];
}

and the function that uses it:

fn dist<P>(a: &P, b: &P) -> P::T
where
    P: Point,
    [(); P::N]: ,
{
    // implementation goes here ...
}

An example of the Point trait being used as intended:

#[derive(Copy, Clone)]
struct MyCustomPoint { a: f64, b: f64 }

impl Point for MyCustomPoint {
    type T = f64;
    const N: usize = 2;

    fn as_array(&self) -> [Self::T; Self::N] {
        [self.a, self.b]
    }
}

The problem

If I implement the Point trait for [f32;N], I get the following issue:

error[E0308]: mismatched types
  --> src\main.rs:63:9
   |
63 |         *self
   |         ^^^^^ expected `Self::N`, found `N`
   |
   = note: expected type `Self::N`
              found type `N`

The code which causes the problem:

impl<const N: usize> Point for [f32; N] {
    type T = f32;
    const N: usize = N;

    fn as_array(&self) -> [Self::T; Self::N] {
        *self
    }
}

Why does the code below cause a mismatched types error, when using a number in the code works fine?

impl Point for [f32; 3] {
    type T = f32;
    const N: usize = 3;

    fn as_array(&self) -> [Self::T; Self::N] {
        *self
    }
}

All the code put together in one block:

#![feature(adt_const_params)]
#![feature(generic_const_exprs)]

use num::Float;

pub trait Point: Copy + Clone {
    type T: num::Float;
    const N: usize;

    fn as_array(&self) -> [Self::T; Self::N];
}

fn dist<P>(a: &P, b: &P) -> P::T
where
    P: Point,
    [(); P::N]: ,
{
    let mut dist_sq: P::T = num::zero();
    for i in 0..P::N {
        let delta = (a.as_array())[i] - (b.as_array())[i];
        dist_sq = dist_sq + delta * delta;
    }
    dist_sq.sqrt()
}

// Works
#[derive(Copy, Clone)]
struct MyCustomPoint { a: f64, b: f64 }

impl Point for MyCustomPoint {
    type T = f64;
    const N: usize = 2;

    fn as_array(&self) -> [Self::T; Self::N] {
        [self.a, self.b]
    }
}

// Works
// impl Point for [f32; 3] {
//     type T = f32;
//     const N: usize = 3;

//     fn as_array(&self) -> [Self::T; Self::N] {
//         *self
//     }
// }

// Doesn't work
impl<const N: usize> Point for [f32; N] {
    type T = f32;
    const N: usize = N;

    fn as_array(&self) -> [Self::T; Self::N] {
        *self
    }
}

fn main() {
    let a = [0f32, 1f32, 0f32];
    let b = [3f32, 1f32, 4f32];
    assert_eq!(dist(&a, &b), 5f32);
}

Also, I've discovered that the following is a workaround.

fn as_array(&self) -> [Self::T; Self::N] {
    // *self

    // Workaround - replace with something simpler if possible.
    let mut array = [0f64; Self::N];
    array[..Self::N].copy_from_slice(&self[..Self::N]);
    array
}
1

There are 1 best solutions below

0
On

Using only stable Rust, I believe you can achieve what you want. Assuming the effect you want is simply to be able to call dist() on arrays of arbitrary (but the same) dimension(s), or Point objects that have the same generic parameter values and return the same size array from .as_array().

Adding the .as_array() method to all arrays of float values, follows a common pattern of declaring a trait to retroactively add features to existing types. Using an array as the implementer in the example is a little odd - all this work to return a copy of itself; but this approach should apply to other types as well that make more sense.

use num::Float;

pub trait Point<T: Float, const N: usize>: Copy + Clone {
    fn as_array(&self) -> [T; N];
}

impl<T: Float, const N: usize> Point<T, N> for [T; N] {
    fn as_array(&self) -> [T; N] {
        *self
    }
}

Then a function that takes the right combination of generics for the compiler to tailor it to its parameters.

fn dist<P, T, const N: usize>(a: &P, b: &P) -> T
where
    P: Point<T, N>,
    T: Float,
{
    let mut dist_sq: T = num::zero();
    for i in 0..N {
        let delta = (a.as_array())[i] - (b.as_array())[i];
        dist_sq = dist_sq + delta * delta;
    }
    dist_sq.sqrt()
}

Which enables code like:

fn main() {
    let a = [0f32, 1f32, 0f32];
    let b = [3f32, 1f32, 4f32];
    assert_eq!(dist(&a, &b), 5f32);
    println!("dist(&a, &b): {}", dist(&a, &b));
    
    let c = [1f64, 2f64, 3f64, 4f64,  5f64];
    let d = [6f64, 7f64, 8f64, 9f64, 10f64];
    
    println!("dist(&c, &d): {}", dist(&c, &d));
}

A const generic type (trait Foo<const A: Float>) has a different purpose than a const associated type, (trait Foo { const A: Float; }). Const generics make a trait more flexible, while associated types make a trait more constrained and specific. So assigning an associated type from a generic parameter (trait Foo<const A: Float> { const A: Float = A;) is mixing two features that have opposite intent and doesn't make much sense.

// Doesn't work
impl<const N: usize> Point for [f32; N] {
    type T = f32;
    const N: usize = N;

    fn as_array(&self) -> [Self::T; Self::N] {
        *self
    }
}

If you want a truly polymorphic interface that virtually any type can implement to interact with other vastly different types that also implement it, this can be achieved by minimizing or eliminating the use of generics altogether from the common trait.

A design choice we'll make below will be to have f64 as the common type produced by Point for the purpose of calculations regardless of what the underlying types' internal representations of the data are. This should allow the needed precision.

Another design choice below is to have Points return an iterator to their coordinate values in a common format (f64). This decouples the internal representation of the Point's data from the interface and allows a Point the freedom to implement its iterator in whatever way is most efficient, either in terms of performance or ergonomics. This way, the point object doesn't have to create an array of a specific size, fill it, and return it on the stack.

use rand::random;
use num::Float;

// A trait that avoids generics and assoc types providing a polymorphic dynamic
// interface.
//
trait Point {
    fn distance(&self, other: &dyn Point) -> f64 
    {
        self.coords().zip(other.coords())
            .fold(0.0, |acc, (a, b)| acc + (a - b) * (a - b))
            .sqrt()
    }
    fn coords(&self) -> Box<dyn Iterator<Item=f64>>;
}

// Implemented for all float arrays.
//
impl<T: 'static + Float + Into<f64>, const N: usize> Point for [T; N] 
{
    fn coords(&self) -> Box<dyn Iterator<Item=f64>> 
    {
        Box::new(self.clone().into_iter().map(|n| n.into()))
    }
}

// Another point object type.
//
struct RandomPoint;

impl Point for RandomPoint {
    fn coords(&self) -> Box<dyn Iterator<Item=f64>>
    {
        Box::new((0..10).map(|_| random::<f64>()))
    }
}

fn main() {
    let a = [0f32, 1f32, 0f32];
    let b = [3f32, 1f32, 4f32];
    println!("a.distance(&b): {}", a.distance(&b));
    
    let c = [0f64, 1.,  2.,  3.,  4.,  5.,  6.,  7.];
    let d = [8f64, 9., 10., 11., 12., 13., 14., 16.];
    println!("c.distance(&d): {}", c.distance(&d));
    
    let r = RandomPoint;

    // We can mix and match the parameters to `.distance()`.
    //
    println!("a.distance(&r): {}", a.distance(&r));  // [f32] & RandomPoint
    println!("a.distance(&d): {}", a.distance(&d));  // [f32] & [f64]
}