Rust 1.51 -- const generics examples

Stack allocated vectors. A wrapper type machinery allows for vector arithmetic with operator chaining. The goal is ideal code generation: Better loop unrolling and dispense of bounds checks due to known length at compile time, no heap allocations and thus no cache fragmentation. The wrapper type functions should be chained at compile time by inlining.

use std::ops::{Add, Sub, Mul};

pub struct Vector<const N: usize>(pub [i32; N]);
pub struct Addition<A: Get, B: Get>(A, B);
pub struct ScalarMul<A: Get>(i32, A);

impl<const N: usize> Vector<N> {
    pub fn set<A: Get>(&mut self, a: A) {
        for (i, v) in self.0.iter_mut().enumerate() {
            *v = a.get(i);
        }
    }
}

impl<const N: usize> std::fmt::Display for Vector<N> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{:?}", self.0)
    }
}

pub trait Get {
    fn len(&self) -> usize;
    fn get(&self, i: usize) -> i32;
}
impl<const N: usize> Get for &Vector<N> {
    fn len(&self) -> usize {self.0.len()}
    fn get(&self, i: usize) -> i32 {self.0[i]}
}
impl<A: Get, B: Get> Get for Addition<A, B> {
    fn len(&self) -> usize {self.0.len()}
    fn get(&self, i: usize) -> i32 {
        self.0.get(i) + self.1.get(i)
    }
}
impl<A: Get> Get for ScalarMul<A> {
    fn len(&self) -> usize {self.1.len()}
    fn get(&self, i: usize) -> i32 {
        self.0*self.1.get(i)
    }
}

impl<'a, B: Get, const N: usize> Add<B> for &'a Vector<N> {
    type Output = Addition<&'a Vector<N>, B>;
    fn add(self, b: B) -> Self::Output {
        Addition(self, b)
    }
}
impl<'a, B: Get, const N: usize> Sub<B> for &'a Vector<N> {
    type Output = Addition<&'a Vector<N>, ScalarMul<B>>;
    fn sub(self, b: B) -> Self::Output {
        Addition(self, ScalarMul(-1, b))
    }
}
impl<A0: Get, A1: Get, B: Get> Add<B> for Addition<A0, A1> {
    type Output = Addition<Self, B>;
    fn add(self, b: B) -> Self::Output {
        Addition(self, b)
    }
}
impl<A0: Get, A1: Get, B: Get> Sub<B> for Addition<A0, A1> {
    type Output = Addition<Self, ScalarMul<B>>;
    fn sub(self, b: B) -> Self::Output {
        Addition(self, ScalarMul(-1, b))
    }
}
impl<A: Get, B: Get> Add<B> for ScalarMul<A> {
    type Output = Addition<Self, B>;
    fn add(self, b: B) -> Self::Output {
        Addition(self, b)
    }
}
impl<A: Get, B: Get> Sub<B> for ScalarMul<A> {
    type Output = Addition<Self, ScalarMul<B>>;
    fn sub(self, b: B) -> Self::Output {
        Addition(self, ScalarMul(-1, b))
    }
}
impl<'a, const N: usize> Mul<&'a Vector<N>> for i32 {
    type Output = ScalarMul<&'a Vector<N>>;
    fn mul(self, a: &'a Vector<N>) -> Self::Output {
        ScalarMul(self, a)
    }
}
impl<A: Get, B: Get> Mul<Addition<A, B>> for i32 {
    type Output = ScalarMul<Addition<A, B>>;
    fn mul(self, a: Addition<A, B>) -> Self::Output {
        ScalarMul(self, a)
    }
}
impl<B: Get, const N: usize> Mul<B> for &Vector<N> {
    type Output = i32;
    fn mul(self, b: B) -> i32 {
        let a = &self.0;
        let mut acc = 0;
        for (i, ai) in a.iter().enumerate() {
            acc += ai*b.get(i);
        }
        acc
    }
}
impl<A0: Get, A1: Get, B: Get> Mul<B> for Addition<A0, A1> {
    type Output = i32;
    fn mul(self, b: B) -> i32 {
        let mut acc = 0;
        for i in 0..self.len() {
            acc += self.get(i)*b.get(i);
        }
        acc
    }
}

fn main() {
    let v = &Vector([1, 2]);
    let w = &Vector([3, 4]);
    let mut buf = Vector([0, 0]);
    buf.set(2*v + 4*w + 5*(v - 2*w));
    println!("{}", buf);
    println!("{}", (v + w)*(v - w));
    println!("{}", v*v - w*w);
}
1 Like

Vector is already taken; Using the same name for a non-growable leads to confusion.

This is basically arrayvec, which uses const generics as of 0.6.0.

Well, that's the stack-allocated Vec at least, but not all the vector-math operations. I expect other crates in that area will embrace const generics too.

1 Like

A system of measurement. In the following an implementation of the MKS system is shown. Every coherent unit of measurement is described by its dimension, i.e. a triple (L, M, T), encoding length to the power of L × mass to the power of M × time to the power of T.

pub mod quantities {
    use std::ops::{Add, Mul};

    #[derive(Clone, Copy)]
    pub struct Quantity<const L: i32, const M: i32, const T: i32> {
        value: f64
    }

    impl<const L: i32, const M: i32, const T: i32>
    Add<Quantity<L, M, T>> for Quantity<L, M, T>
    {
        type Output = Quantity<L, M, T>;
        fn add(self, y: Quantity<L, M, T>) -> Quantity<L, M, T> {
            Quantity {value: self.value + y.value}
        }
    }

    impl<const L: i32, const M: i32, const T: i32>
    Mul<Quantity<L, M, T>> for f64
    {
        type Output = Quantity<L, M, T>;
        fn mul(self, y: Quantity<L, M, T>) -> Quantity<L, M, T> {
            Quantity {value: self*y.value}
        }
    }

    // speed*speed
    impl Mul<Quantity<0, 1, -1>> for Quantity<0, 1, -1> {
        type Output = Quantity<0, 2, -2>;
        fn mul(self, y: Quantity<0, 1, -1>) -> Self::Output {
            Quantity {value: self.value*y.value}
        }
    }

    // mass*(speed squared)
    impl Mul<Quantity<0, 2, -2>> for Quantity<1, 0, 0> {
        type Output = Quantity<1, 2, -2>;
        fn mul(self, y: Quantity<0, 2, -2>) -> Self::Output {
            Quantity {value: self.value*y.value}
        }
    }

    // momentum*speed
    impl Mul<Quantity<0, 1, -1>> for Quantity<1, 1, -1> {
        type Output = Quantity<1, 2, -2>;
        fn mul(self, y: Quantity<0, 1, -1>) -> Self::Output {
            Quantity{value: self.value*y.value}
        }
    }

    // mass*speed
    impl Mul<Quantity<0, 1, -1>> for Quantity<1, 0, 0> {
        type Output = Quantity<1, 1, -1>;
        fn mul(self, y: Quantity<0, 1, -1>) -> Self::Output {
            Quantity {value: self.value*y.value}
        }
    }

    pub type Mass = Quantity<1, 0, 0>;
    pub type Speed = Quantity<0, 1, -1>;
    pub type Energy = Quantity<1, 2, -2>;

    use std::fmt;
    impl fmt::Display for Energy {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{} Joule", self.value)
        }
    }

    pub mod unit {
        use std::ops::Mul;
        use super::{Mass, Speed};

        pub struct Kilogramm;
        pub struct MeterPerSecond;

        impl Mul<Kilogramm> for f64 {
            type Output = Mass;
            fn mul(self, _y: Kilogramm) -> Self::Output {
                Mass {value: self}
            }
        }
        impl Mul<MeterPerSecond> for f64 {
            type Output = Speed;
            fn mul(self, _y: MeterPerSecond) -> Self::Output {
                Speed {value: self}
            }
        }
        
    }
}

use quantities::{Mass, Speed, Energy};
use quantities::unit::{Kilogramm, MeterPerSecond};

fn kinetic_energy(m: Mass, v: Speed) -> Energy {
    0.5*m*v*v
}

fn main() {
    let m = 1.0*Kilogramm;
    let v = 2.0*MeterPerSecond;
    println!("{}", kinetic_energy(m, v));
}
2 Likes

Stuff like iter_mut() will in no way take advantage of the constant N in the way you might be hoping, FYI, as when you call it you're first turning the array into a full-length slice reference that does not have the constant N in scope at all.

I learned over time while working on my crate staticvec that you really do want to be keeping N (or any other relevant constant generic parameters) as close to as directly in scope as is possible as often as is possible to get the best optimization.

1 Like

This looks kind of disenheartening, we can't yet define multiplication generically?

In nightly it almost works. Currently, associated types do not allow it, but you can do:

#![allow(incomplete_features)]
#![feature(const_generics)]
#![feature(const_evaluatable_checked)]

pub trait MulOp<Rhs, Output> {
    fn mul(self, rhs: Rhs) -> Output;
}

impl<const L1: i32, const M1: i32, const T1: i32,
     const L2: i32, const M2: i32, const T2: i32>
MulOp<
    Quantity<L2, M2, T2>,
    Quantity<{L1 + L2}, {M1 + M2}, {T1 + T2}>
> for Quantity<L1, M1, T1> {
    fn mul(self, rhs: Quantity<L2, M2, T2>)
    -> Quantity<{L1 + L2}, {M1 + M2}, {T1 + T2}>
    {
        Quantity {value: self.value*rhs.value}
    }
}

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.