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
}
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.Then a function that takes the right combination of generics for the compiler to tailor it to its parameters.
Which enables code like:
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.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 byPoint
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
Point
s return an iterator to their coordinate values in a common format (f64
). This decouples the internal representation of thePoint
's data from the interface and allows aPoint
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.