最近在学习Rust,尝试用Rust写一个矩阵和向量运算时用到了一些宏和泛型,做个笔记。
为了支持不同维度的向量,维度N就只能通过参数的方法传入,如果将N作为参数,向量就可以这么定义:
struct Vector {
pub dimension: usize,
pub data: Vector<f32>,
}
Vec 的变长在这里并不必要,直接用数组可以减少向量长度上的判断,直接提前到编译期,于是用到了常量泛型参数:
struct Vector<const N: usize> {
pub data: [f32; N],
}
struct Matrix<const N: usize> {
pub data: [Vector<N>; N],
}
这么做可以直接在编译的时候就确定数组长度,减少不必要的运行时内存操作。在定义运算的时候,指定相同的参数 N 就能强制要求只有相同长度的向量才能进行运算,比如:
impl<const N: usize> Add for &Vector<N> {
type Output = Vector<N>;
fn add(self, rhs: Self) -> Self::Output {
self.data.iter().zip(rhs.data.iter()).map(|(&x, &y)| x + y).collect::<Vector<_>>().try_into().unwarp()
}
}
对应的元素运算是直接通过实现运算符 trait 来做的,同样可以保证维数相同,但是分开实现五个运算符(Add,Sub,Mul,Div,Neg)的重复代码太多,而且为了方便,其中四个还要实现右操作数为 f32 的版本,因此很自然地想到了用宏来缩短。关于泛型常量参数,更详细介绍的推荐浅聊泛型常量参数 Const Generic。
Rust 主要有两种宏,过程宏和声明宏。和 C 系宏不同的是,Rust 里的宏是需要经过 AST 解析然后再展开的,并不像C语言那样是主要是文本操作,而且过程宏相较于声明宏太复杂,虽然灵活性更高,但是限制也更大。
f32 本身也实现了相应的 trait,因此就可以这么写:
macro_rules! impl_compo_op {
($type:ident, $op_trait:ident, $op_fun:ident) => {
impl<const N: usize> $op_trait<&$type<N>> for &$type<N> {
type Output = $type<N>;
fn $op_fun(self, other: &$type<N>) -> $type<N> {
$type {
data: self.data.iter().zip(other.data.iter()).map(|(a, b)| a.$op_fun(b)).collect::<Vec<_>>().try_into().unwrap(),
}
}
}
impl<const N: usize> $op_trait<f32> for &$type<N> {
type Output = $type<N>;
fn $op_fun(self, scalar: f32) -> $type<N> {
$type {
data: self.data.iter().map(|&x| x.$op_fun(scalar)).collect::<Vec<_>>().try_into().unwrap(),
}
}
}
}
}
impl_compo_op!(Vector, Add, add);
impl_compo_op!(Vector, Sub, sub);
impl_compo_op!(Vector, Mul, mul);
impl_compo_op!(Vector, Div, div);
并且这个宏还能直接用在 matrix上。
但是泛型常量参数也有个比较大的限制——不能用作运算。再需要泛型常量的地方是可以用表达式的,比如:
const X: usize = 1;
fn main() {
let m4: Matrix<{X + 3}> = Matrix::new_identity();
let data: [Vector<{X + 3}>; {2 + 2}] = m4.data;
}
这些运算都能再编译期完成,并且是合法的。但是这个表达式中不能有泛型常参,比如:
impl<const N: usize> Matrix<N> {
fn minor(&self, row: usize, col: usize) -> Matrix<{N - 1}> {
// ..
}
}
这种代码在Rust中是不允许的:
Rust Reference - Generic parameters: This syntactic restriction is necessary to avoid requiring infinite lookahead when parsing an expression inside of a type.
并且暂时还没有找到方法解决。另外,当前的声明式宏也不能匹配泛型常量参数:
impl_compo_op!(Vector<N>, Add, add); // Error
不过暂时影响不大。