|
| 1 | +# 😱 Status quo: nalgebra |
| 2 | + |
| 3 | +*a huge thanks to [Andreas Borgen Longva](https://github.com/Andlon) and [Sébastien Crozet](https://github.com/sebcrozet) for the help with figuring this out* |
| 4 | + |
| 5 | +[nalgebra](https://nalgebra.org/) is a linear algebra library. At the core of that library is a type `struct Matrix<T, R, C, S>` where `T` is the components scalar type, `R` and `C` represents the number of rows and columns and `S` represents the type of the buffer containing the data. |
| 6 | + |
| 7 | +Relevant for const generics are the parameters `R` and `C`. These are instantiated using one of the following types: |
| 8 | +```rust |
| 9 | +// For matrices of know size. |
| 10 | +pub struct Const<const R: usize>; |
| 11 | +// For matrices with a size only known at runtime. |
| 12 | +pub struct Dynamic { value: usize } |
| 13 | +``` |
| 14 | + |
| 15 | +The authors of nalgebra then introduce a type alias |
| 16 | +```rust |
| 17 | +pub struct ArrayStorage<T, const R: usize, const C: usize>(pub [[T; R]; C]); |
| 18 | +/// A matrix of statically know size. |
| 19 | +pub type SMatrix<T, const R: usize, const C: usize> = |
| 20 | + Matrix<T, Const<R>, Const<C>, ArrayStorage<T, R, C>>; |
| 21 | +``` |
| 22 | + |
| 23 | +To deal with the lack of generic const expressions, they add a trait for conversions from and to [`typenum`](https://crates.io/crates/typenum) for all `Const` up to size `127` ([source](https://github.com/dimforge/nalgebra/blob/39bb572557299a44093ea09daaff144fd6d9ea1f/src/base/dimension.rs#L273-L345)). |
| 24 | + |
| 25 | +Whenever they now need some computation using `Const<N>`, they convert it to type nums, evaluate the computation using the trait system, and then convert the result back to some `Const<M>`. |
| 26 | + |
| 27 | +## Disadvantages |
| 28 | + |
| 29 | +While this mostly works fine, there are some disadvantages. |
| 30 | + |
| 31 | +### Annoying `ToTypenum` bounds |
| 32 | + |
| 33 | +Most notably this adds a lot of unnecessary bounds, consider the following impl: |
| 34 | + |
| 35 | +```rust |
| 36 | +impl<T, const R1: usize, const C1: usize, const R2: usize, const C2: usize> |
| 37 | + ReshapableStorage<T, Const<R1>, Const<C1>, Const<R2>, Const<C2>> for ArrayStorage<T, R1, C1> |
| 38 | +where |
| 39 | + T: Scalar, |
| 40 | + Const<R1>: ToTypenum, |
| 41 | + Const<C1>: ToTypenum, |
| 42 | + Const<R2>: ToTypenum, |
| 43 | + Const<C2>: ToTypenum, |
| 44 | + <Const<R1> as ToTypenum>::Typenum: Mul<<Const<C1> as ToTypenum>::Typenum>, |
| 45 | + <Const<R2> as ToTypenum>::Typenum: Mul< |
| 46 | + <Const<C2> as ToTypenum>::Typenum, |
| 47 | + Output = typenum::Prod< |
| 48 | + <Const<R1> as ToTypenum>::Typenum, |
| 49 | + <Const<C1> as ToTypenum>::Typenum, |
| 50 | + >, |
| 51 | + >, |
| 52 | +{ |
| 53 | + type Output = ArrayStorage<T, R2, C2>; |
| 54 | + |
| 55 | + fn reshape_generic(self, _: Const<R2>, _: Const<C2>) -> Self::Output { |
| 56 | + unsafe { |
| 57 | + let data: [[T; R2]; C2] = mem::transmute_copy(&self.0); |
| 58 | + mem::forget(self.0); |
| 59 | + ArrayStorage(data) |
| 60 | + } |
| 61 | + } |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +As these bounds infect the public API, they are also a large backwards compatability concern. |
| 66 | + |
| 67 | +### `ToTypenum` is only implemented up to fixed size |
| 68 | + |
| 69 | +That's annoying. ✨ |
| 70 | + |
| 71 | +### Cannot use associated constants |
| 72 | + |
| 73 | +It is currently also not possible to have the size of a matrix depend on associated constants: |
| 74 | +```rust |
| 75 | +trait MyDimensions { |
| 76 | + const ROWS: usize; |
| 77 | + const COLS: usize; |
| 78 | +} |
| 79 | + |
| 80 | +fn foo<Dims: MyDimensions>() { |
| 81 | + // Not possible! |
| 82 | + let matrix: SMatrix<f64, Dims::ROWS, Dims::COLS> = SMatrix::zeros(); |
| 83 | +} |
| 84 | +``` |
| 85 | +While this can be avoided by going to back to `typenum` and using associated types, this adds a lot of unnecessary bounds and inpacts all of the code dealing with it. |
| 86 | + |
| 87 | +### Generic parameters aren't exhaustive |
| 88 | + |
| 89 | +Because `R` and `C` are generic parameters and not constants, the compiler doesn't know that |
| 90 | +`DefaultAllocator: Allocator<T, R, C>` holds for all `R` and `C`, leaking implementation defaults |
| 91 | +and causing signatures to be far less readable than necessary. |
| 92 | + |
| 93 | +## Wishlist |
| 94 | + |
| 95 | +Ideally, `Matrix` could be changed to the following: |
| 96 | + |
| 97 | +```rust |
| 98 | +enum Dim { |
| 99 | + Const(usize), |
| 100 | + Dynamic, |
| 101 | +} |
| 102 | + |
| 103 | +struct Matrix<T, const R: Dim, const C: Dim, S> { ... } |
| 104 | + |
| 105 | +type SMatrix<T, const R: usize, const C: usize> = |
| 106 | + Matrix<T, Dim::Const(R), Dim::Const(C), ArrayStorage<T, R, C>>; |
| 107 | +``` |
| 108 | + |
| 109 | +For this to work well there have a bunch of requirements for const generics: |
| 110 | + |
| 111 | +### User-defined types as const parameter types |
| 112 | + |
| 113 | +We have to be able to use `Dim` as a const param type |
| 114 | + |
| 115 | +### Consider injective expressions to bind generic params |
| 116 | + |
| 117 | +With this change, `nalgebra` needs impls like the following |
| 118 | + |
| 119 | +```rust |
| 120 | +impl<T, const R: usize, const C: usize> for SMatrix<T, R, C> { |
| 121 | + // ... |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +For this impl to bind `R` and `C`, the expression `Dim::Const(N)` has to bind `N`. |
| 126 | +This is sound as constructors are injective. It seems very desirable to at least |
| 127 | +enable this for expressions using constructors. |
| 128 | + |
| 129 | +Without this, one gets an error message like the following: |
| 130 | +``` |
| 131 | +error[E0207]: the const parameter `R` is not constrained by the impl trait, self type, or predicates |
| 132 | + --> src/lib.rs:5:12 |
| 133 | + | |
| 134 | +5 | impl<T, const R: usize, const C: usize> for SMatrix<T, R, C> { |
| 135 | + | ^ unconstrained const parameter |
| 136 | + | |
| 137 | + = note: expressions using a const parameter must map each value to a distinct output value |
| 138 | + = note: only used in the expression `Dim::Const(R)` |
| 139 | + = note: proving the result of expressions other than the parameter are unique is not supported |
| 140 | +``` |
| 141 | + |
| 142 | +### Merge partial impls to be exhaustive |
| 143 | + |
| 144 | +By adding one trait impl impl for `Dim::Dynamic` and one for `Dim::Const(N)`, it should be enough to consider that trait to be implemented for all `Dim`. |
| 145 | + |
| 146 | +Ideally, the compiler should figure this out by itself, or it can be emulated using specialization by manually adding an impl for all `Dim` which always gets overridden. |
| 147 | + |
| 148 | +### Generic const expressions |
| 149 | + |
| 150 | +For example when computing the [Kronecker product](https://en.wikipedia.org/wiki/Kronecker_product) which has the following simplified signature: |
| 151 | +```rust |
| 152 | +pub fn kronecker<T, const R1: Dim, const C1: Dim, const R2: Dim, const C2: Dim>( |
| 153 | + lhs: &Matrix<T, R1, C2>, |
| 154 | + rhs: &Matrix<T, R2, C2>, |
| 155 | +) -> Matrix<T, R1 * R2, C1 * C2> { |
| 156 | + ... |
| 157 | +} |
| 158 | +``` |
| 159 | + |
| 160 | +For this generic const expressions have to be supported. |
| 161 | + |
| 162 | +### const Trait implementations |
| 163 | + |
| 164 | +For `R1 * R2` to work we need const trait impls, otherwise this |
| 165 | +can be written using `mul_dim(R1, R2)` or something. |
| 166 | + |
| 167 | +## `Default` for arrays |
| 168 | + |
| 169 | +`nalgebra` currently has to work around `Default` not being implemented |
| 170 | +for all arrays where `T: Default`. |
0 commit comments