Skip to content

Commit a33bec1

Browse files
authored
Merge pull request #19 from lcnr/nalgebra
add story about nalgebra
2 parents c1e2734 + 9da02e1 commit a33bec1

File tree

2 files changed

+171
-0
lines changed

2 files changed

+171
-0
lines changed

SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- [😱 Status quo](./vision/status_quo.md)
1010
- [Array defaults](./vision/status_quo/array_default.md)
1111
- [Array split first method](./vision/status_quo/split_first.md)
12+
- [nalgebra](./vision/status_quo/nalgebra.md)
1213
- [✨ Shiny future](./vision/shiny_future.md)
1314
- [Array defaults](./vision/shiny_future/array_default.md)
1415
- [Array split first method](./vision/shiny_future/split_first.md)

vision/status_quo/nalgebra.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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

Comments
 (0)