Nalgebra is a powerhouse of functionality, but its documentation can be overwhelming—the documentation for Matrix
lists over 600 methods. Your documentation endeavors might not be quite so overwhelming, but you still could benefit from these three tricks nalgebra uses to improve its docs.
Documenting Type Aliases
TIP: Write impl
s and documentation on type aliases.
The Matrix
struct
type is at the heart of nalgebra’s functionality. It is generically parametrized by dimension, so the same outer type is used to encode matrices of all sizes. A vector of dimension R
, for instance, is merely a Matrix
of R
rows and U1
columns.
From a programming ergonomics perspective, you might think it’d be convenient to codify this with a type alias; e.g.:
type Vector<N, D, S> = Matrix<N, D, U1, S>;
…and you’d be right; nalgebra defines just such a type alias!
But type aliases aren’t just programming shorthand; they can be used to improve documentation, too: When an inherent impl
is written in terms of a type alias, the documentation of that impl
also appears of the documentation page of that type alias.
Sure enough, if you visit nalgebra’s Vector
documentation page type alias, you’ll see only the methods specific to vectors:
Unfortunately, this same documentation is also rendered on the page for Matrix
and without the type aliases. This is why the documentation for the base Matrix
type is so long. :-(
Coalescing impl
s
TIP: Reduce repetition by grouping methods with the same bounds into the a single bounded impl
.
One of nalgebra’s cooler ergonomic shortcuts is vector swizzling. A swizzle lets you build a new vector from some ordering and subset of the components of another vector. For instance, vec.xyx()
constructs a new, three-dimensional vector comprised of the x
, y
and x
components of vec
.
Supporting this shortcut requires generating a lot of methods. A couple years ago, the documentation of these methods looked like this:
This documentation has a very poor signal-to-noise ratio. The preamble
impl<N: Scalar, D: DimName, S: Storage<N, D>> Vector<N, D, S>
is repeated for every swizzling method, and each individual swizzling method has its own where
bound documenting its dimensionality requirements.
Nearly all of this repetition was eliminated with a minor change to the macro generating these methods:
What changed? The old macro generated an impl
for each swizzling method:
impl<N: Scalar, D: DimName, S: Storage<N, D>> Vector<N, D, S> {
pub fn xx(&self) -> Vector2<N>
where
D::Value: Cmp<typenum::U0, Output=Greater>
{ ... }
}
impl<N: Scalar, D: DimName, S: Storage<N, D>> Vector<N, D, S> {
pub fn xxx(&self) -> Vector3<N>
where
D::Value: Cmp<typenum::U0, Output=Greater>
{ ... }
}
impl<N: Scalar, D: DimName, S: Storage<N, D>> Vector<N, D, S> {
pub fn xy(&self) -> Vector2<N>
where
D::Value: Cmp<typenum::U1, Output=Greater>
{ ... }
}
/* and so on */
The new macro groups the methods into one of just three impl
s depending on their dimensionality requirements:
// Swizzling methods for Vectors of dimension > 0
impl<N: Scalar, D: DimName, S: Storage<N, D>> Vector<N, D, S>
where
D::Value: Cmp<typenum::U0, Output=Greater>
{
pub fn xx(&self) -> Vector2<N>
where
D::Value: Cmp<typenum::U0, Output=Greater>
{ ... }
pub fn xxx(&self) -> Vector3<N>
where
D::Value: Cmp<typenum::U0, Output=Greater>
{ ... }
}
// Swizzling methods for Vectors of dimension > 1
impl<N: Scalar, D: DimName, S: Storage<N, D>> Vector<N, D, S>
where
D::Value: Cmp<typenum::U1, Output=Greater>
{
pub fn xy(&self) -> Vector2<N>
{ ... }
/* and so on */
}
// Swizzling methods for Vectors of dimension > 2
impl<N: Scalar, D: DimName, S: Storage<N, D>> Vector<N, D, S>
where
D::Value: Cmp<typenum::U2, Output=Greater>
{
pub fn xz(&self) -> Vector2<N>
{ ... }
/* and so on */
}
…and rustdoc faithfully adheres to this organization when generating nalgebra’s documentation!
If you are generating impl
s via a macro, check if your macro could be tweaked to group similar methods into the same impl
!
Documenting impl
s
TIP: You can write documentations on individual impl
s!
Like Rust’s slices, nalgebra’s arrays allow for overloaded indexing; e.g.:
let matrix = Matrix3::new(0, 3, 6,
1, 4, 7,
2, 5, 8);
// index a particular element
assert_eq!(matrix.index((0, 0)), &0);
// select a range of rows and all columns
assert!(matrix.index((1..3, ..))
.eq(&Matrix2x3::new(1, 4, 7,
2, 5, 8)));
…and these overloaded index types are usable with a whole suite of associated methods: index
, index_mut
, get
, get_mut
, get_unchecked
and get_unchecked_mut
. The same indexing types can be used on each of these methods—they only differ in their fallibility, mutability, and safety.
These six methods are grouped into the same impl
. The documentation for the individual methods focuses just on their differences. Their similarities (namely, the different kinds of indexes which can be used) are documented on this shared impl
:
If you have thematically similar methods, you can group them into their own impl
, and write rustdoc on that impl
!
Concretely:
/// # Indexing Operations
/// [documentation about indexing as a whole]
impl<N: Scalar, R: Dim, C: Dim, S: Storage<N, R, C>> Matrix<N, R, C, S> {
/// [documentation *just* for `get`]
#[inline]
pub fn get<'a, I>(&'a self, index: I) -> Option<I::Output>
where
I: MatrixIndex<'a, N, R, C, S>,
{ ... }
/// [documentation *just* for `get_mut`]
#[inline]
pub fn get_mut<'a, I>(&'a self, index: I) -> Option<I::Output>
where
S: StorageMut<N, R, C>,
I: MatrixIndexMut<'a, N, R, C, S>,
{ ... }
/* and so on */
}