I'm struggling to work around the limitation that in Rust, you cannot use a const generic parameter in an expression to specify a type. I am still learning rust and am trying to make a matrix type where I calculate the determinant using Laplace expansion like this:
#[derive(Clone, Eq, Debug)]
pub struct Matrix<T: Copy + Eq, const M: usize, const N: usize> {
pub data: [[T; N]; M],
}
impl<T: Copy + Eq + ops::Mul + std::iter::Sum, const S: usize> Matrix<T, S, S>
where
i32: Into<T>,
{
pub fn det(&self) -> T {
if S == 1 {
self.data[0][0]
} else {
(0..S)
.map(|c| {
Into::<T>::into(if c % 2 == 0 { 1 } else { -1 })
* self.data[0][c]
* Matrix::<T, { S - 1 }, { S - 1 }> {
data: core::array::from_fn(|i| {
core::array::from_fn(|j| {
self.data[i + 1][if j < c { j } else { j + 1 }]
})
}),
}
.det()
})
.sum()
}
}
}
However, Matrix::<T, {S-1}, {S-1}>
is not valid rust! How would I work around this to get my determinant function to work how I want?
As is, I don't think your code can be compiled in Rust.
The main problem you are facing is the recursive structure of your trait bounds:
S
, you need the determinant at sizeS - 1
, which requires thatS - 1
be strictly positive, ie[(); S - 1]: Sized
.S - 1
will in turn require the determinant at sizeS - 2
, which requires thatS - 2
be strictly positive, etc...Since the Rust compiler only reasons syntactically about bounds -- ie,
S - 1 - 1
is different fromS - 2
as far as it's concerned -- this leads to requiring an infinite number of bounds.Traits?
One common mistake in generics in Rust, is attempting to make generics work with structs, rather than traits. In this case, however, I fail to see how traits could be applied without suffering from the infinite recursion issue.
Specialization?
That's a whole other can of worms...
Pivoting
Disclaimer: untested code.
From a pragmatic point of view, I'd recommend abandoning the idea of creating a compile-time sized matrix inside the code.
You're creating matrices of size
S - 1
,S - 2
, ... The sum of squares from 0 to n isn * (n - 1) * (2n - 1) / 6
, so that's how much scratch space you'll need, total. You can simply reserve an array of that size1 once, then feed it to helper code as scratch space.Roughly speaking, I am imagining something like (nightly required):
1 Though, given how quickly it grows, maybe you'll need a
Vec
to avoid a stack overflow. Then again, the calculation will take forever on largeS
anyway.Compact
Disclaimer: untested code.
There's something to be said for a more compact solution.
In the end, there's not much advantage in creating a fresh sub-matrix, and using it once (to create newer sub-matrices). It costs quite a bit to create it, and there's no recouping since it's not used much.
What if, instead, we created a view over the original matrix? The rows are easily handled, though the columns are a tad more complicated, so we'll have a stack usage that grows with the square of
S
... which is still better than growing with its cube.This is likely quite faster than creating new matrices again and again, as the number of (immediate) writes per call to
det
is linear with the size of the sub-matrix, rather than quadratic.The number of reads is unchanged -- the overall algorithm being unchanged -- but writes being more costly than reads, it should be a performance win at least.