Несоответствующие типы при использовании связанных констант и постоянных обобщений в Rust nightly

#rust #const-generics #associated-const

Вопрос:

Итак, для библиотеки, которую я пишу, я хочу вычислить расстояние между двумя точками в N измерениях (2, 3, 4 и т. Д.), И у меня есть точечный признак, чтобы пользователи библиотеки могли использовать эту функцию для своих собственных типов, если они «точечные».

У меня есть признак «Point», размеры которого (N) и тип с плавающей запятой (T) сохраняются как связанный тип и константа:

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

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

и функция, которая его использует:

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

Пример использования точечного признака по назначению:

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

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

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

 

Проблема

Если я реализую точечный признак для [f32;N], я получаю следующую проблему:

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

Код, вызывающий проблему:

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

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

Почему приведенный ниже код вызывает ошибку несоответствующих типов, когда использование числа в коде работает нормально?

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

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

Весь код, собранный в одном блоке:

 #![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(amp;self) -> [Self::T; Self::N];
}

fn dist<P>(a: amp;P, b: amp;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(amp;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(amp;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(amp;self) -> [Self::T; Self::N] {
        *self
    }
}

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

 

Кроме того, я обнаружил, что следующее является обходным решением.

 fn as_array(amp;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(amp;self[..Self::N]);
    array
}
 

Комментарии:

1. Const дженерики все еще находятся в разработке и имеют много нерешенных проблем: github.com/rust-lang/rust/labels/A-const-generics . Я бы предположил, что это один из них.

2. Я надеялся, что проблема во мне, а не в компиляторе, ха-ха. Спасибо за ссылку, я собираюсь посмотреть, сообщается ли об этой конкретной проблеме.

3. Я не думаю, что проблема заключается в новых функциях, а в применении существующих стабильных функций дженериков. Работа с некоторыми незначительными изменениями: ссылка на игровую площадку

4. Я остановился на вашем решении, Тодд, оно кажется лучшим для моих целей. В моем проекте используется связанная константа в другом месте, которую я здесь не показал, но это кажется больше проблем, чем того стоит. Спасибо

5. Рад, что я мог бы помочь @scott97, всегда пожалуйста.

Ответ №1:

Я полагаю, что вы можете достичь того, чего хотите, используя только стабильный Rust. Предполагая, что эффект, который вы хотите, — это просто иметь возможность вызывать dist() массивы произвольных (но одинаковых) размеров или точечных объектов, которые имеют одинаковые значения общих параметров и возвращают массив одинакового размера .as_array() .

Добавление .as_array() метода ко всем массивам значений с плавающей запятой следует общему шаблону объявления признака для ретроактивно добавления функций к существующим типам. Использование массива в качестве разработчика в примере немного странно — вся эта работа возвращает копию самого себя; но этот подход должен применяться и к другим типам, которые имеют больше смысла.

 use num::Float;

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

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

Затем функция, которая использует правильную комбинацию обобщений, чтобы компилятор адаптировал ее к своим параметрам.

 fn dist<P, T, const N: usize>(a: amp;P, b: amp;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()
}
 

Что позволяет использовать код, подобный:

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

Универсальный тип const ( trait Foo<const A: Float> ) имеет другое назначение, чем связанный с const тип, ( trait Foo { const A: Float; } ) . Постоянные обобщения делают признак более гибким, в то время как связанные типы делают признак более ограниченным и специфичным. Таким образом, назначение связанного типа из универсального параметра ( trait Foo<const A: Float> { const A: Float = A; ) смешивает две функции, которые имеют противоположные намерения и не имеют особого смысла.

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

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

Если вам нужен действительно полиморфный интерфейс, который может реализовать практически любой тип для взаимодействия с другими совершенно разными типами, которые также его реализуют, этого можно достичь, минимизировав или полностью исключив использование дженериков из общего признака.

Выбор дизайна, который мы сделаем ниже, будет заключаться в том, чтобы f64 использовать в качестве общего типа, создаваемого Point для целей вычислений, независимо от того, каковы внутренние представления данных базовых типов. Это должно обеспечить необходимую точность.

Другой вариант дизайна, приведенный ниже, заключается в том, чтобы Point s возвращали итератору свои значения координат в общем формате ( f64 ). Это отделяет внутреннее представление Point данных от интерфейса и позволяет a Point свободно реализовывать свой итератор любым наиболее эффективным способом, будь то с точки зрения производительности или эргономики. Таким образом, объекту point не нужно создавать массив определенного размера, заполнять его и возвращать в стек.

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

// A trait that avoids generics and assoc types providing a polymorphic dynamic
// interface.
//
trait Point {
    fn distance(amp;self, other: amp;dyn Point) -> f64 
    {
        self.coords().zip(other.coords())
            .fold(0.0, |acc, (a, b)| acc   (a - b) * (a - b))
            .sqrt()
    }
    fn coords(amp;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(amp;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(amp;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(amp;b): {}", a.distance(amp;b));
    
    let c = [0f64, 1.,  2.,  3.,  4.,  5.,  6.,  7.];
    let d = [8f64, 9., 10., 11., 12., 13., 14., 16.];
    println!("c.distance(amp;d): {}", c.distance(amp;d));
    
    let r = RandomPoint;

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