Integrating const-generics to nalgebra 0.26
Today we released the version 0.26 of our general-purpose linear-algebra crate: nalgebra 🎊
The version 1.51.0 of Rust has been released three weeks ago. That version stabilized an MVP for one of the feature we wanted the most: const-generics. Const-generics allow you to define types parametrized by const integers, chars, or booleans. One iconic example is writing a structure wrapping an array of any size:
nalgebra supports matrices (and vectors) with dimensions known at runtime or at compile-time.
The components of matrices/vectors with dimensions known at runtime are stored in a Vec<T>
.
Before nalgebra 0.26, matrices/vectors with dimensions known at compile-time were stored
in a GenericArray
, from the excellent generic-array
crate. Thanks to const-generics, we were able to replace GenericArray
by standard arrays: [[T; R]; C]
(where R
is the number of rows, and C
the number of columns) in nalgebra 0.26.
This change results in significant ergonomics improvements when using statically-sized matrices or vectors:
1. Simpler generic programming with statically-sized entities
Using nalgebra for generic programming with statically-sized matrices/vectors/points was quite challenging before. Let's take a simple example of a generic structure that wraps a point, a vector, and a matrix and performs some kind integration using 64-bit precision. In previous version of nalgebra this would look like this:
See all the DefaultAllocator: Allocator<...>
trait bounds? They are here to help the compiler
deduce the proper storage types for the matrix/vector/points (i.e. to deduce the GenericArray
with the right dimensions).
Now that const-generics have been integrated to nalgebra this code becomes as simple as one would expect:
See that there isn't any DefaultAllocator: Allocator
trait bounds whatsoever.
Now, keep in mind that these simplifications will only work if you stick with entities
with dimensions known at compile-time. If your code needs to work generically for both
statically-sized matrices and dynamically-sized matrices, then the DefaultAllocator: Allocator
bounds are still necessary (we should be able to get rid of them too once specialization
is stabilized).
2. Simpler debugging of small matrices and vectors
This one is a life-changer for those relying extensively on debuggers. Until now,
inspecting with a debugger the content of statically-sized matrices or vectors (or
points, quaternions, etc.) was extremely difficult. This was caused by the smart,
but complicated, recursive definition of GenericArray
. Here is what inspecting
the content of let matrix = Matrix2::new(1, 2, 3, 4)
looked like (in CLion's debugger,
using the Rust plugin):
Here is what it looks like now that we replaced GenericArray
by standard arrays:
This is much clearer now. Keep in mind that nalgebra stores its matrices in
column-major format. That's why the components appear in the order [[1, 3], [2, 4]]
(column
by column).
3. Building small, constant, matrices and vectors
Now that small matrices are using standard arrays under the hood, it is now possible
to define constant vectors, matrices, points, quaternions, and translations by using
their const fn new(...)
constructors:
4. New aliases
Everything in nalgebra ends up being expressed as a matrix, in one way or another. Even
a vector is just the Matrix
type setup to have only one column. This is why we have so many
type aliasses like Vector3
which is an alias for a matrix with 3 rows and one column.
In nalgebra 0.26 we have the following dimension-generic type aliases:
OMatrix<T, R: Dim, C: Dim>
: for owned matrices (formerly calledMatrixMN
), i.e., matrices that own their components (as opposed to matrix slices that borrow them).SMatrix<T, const R: usize, const C: usize>
: for statically-allocated matrices.DMatrix<T>
: for dynamically-allocated matrices.
All the aliases for specific dimensions (e.g. Vector3
for 3D vectors) remain unchanged.
The same applies to vectors: OVector, SVector, DVector
. For example a statically-sized vector
with 528 components can be written SVector<f32, 528>
.
typenum
is still needed
Limitations: why We no longer use the generic-array
crate, but you will notice that nalgebra continues
to depend on the typenum
crate. This is needed because const-generics support on Rust 1.51
isn't complete yet, and doesn't allow operations on the const-parameters like in the following
push
method that adds an element to an array:
In order to overcome this limitation, we have to rely on typenum
by converting the
integer to a type-level-integer, performing the operation with typenum
's type-level operators,
and then extracting the result as a const value. This roughly looks like this:
It's actually less verbose in nalgebra because we have some additional traits to hide some
of the complexity going on here. But the fact remains that without more advanced const-generics
support from the compiler, methods like this push
will be more verbose to define, and will be limited
to the values of D
(currently the values in [0, 127]
) such that Const<D>: TyTypenum
is implemented.
What's next?
The const-generics MVP allowed us to significantly improve the ergonomics of statically-sized matrices and vectors. However, we are still waiting for two features to land to make nalgebra much nicer:
- Specialization: with specialization in place, we could get rid of all the
DefaultAllocator: Allocator<...>
bounds. This means writing generic code that accepts both statically-sized matrices and dynamically-sized matrices would become much easier. - Const-generics (the complete version): being able to write something like
[T;Â {Â LENGTHÂ +Â 1Â }]
would allow us to remove thetypenum
dependency completely. - Const-fn (the complete version): being able to have trait bounds other than
Sized
in generic const-functions would allow us to mark most of nalgebra's methods asconst
. Right now, only some constructors are possible.