Using generic parameters in const operations

I'm defining a struct that will hold the results obtained by a Kalman filter. For my research simulations, I know how much storage I need at compile time, which means I can use const generics to statically allocate arrays. Unfortunately, I am unable to perform const operations on the generic parameters, which means that the following won't compile:

pub struct FilterStorage<const N: usize, const P: usize, const COUNT: usize> {
  mx_prior: [[f64; N]; COUNT],
  mx_post:  [[f64; N]; COUNT],
  ex_prior: [[f64; N]; COUNT],
  ex_post:  [[f64; N]; COUNT],
  sx_prior: [[f64; N]; COUNT],
  sx_post:  [[f64; N]; COUNT],
  mx: [[f64; N]; 2 * COUNT],
  ex: [[f64; N]; 2 * COUNT],
  sx: [[f64; N]; 2 * COUNT],
  
  ez: [[f64; P]; COUNT],
  sz: [[f64; P]; COUNT],
}

I took a look at this StackOverflow question, this GitHub issue, this GitHub issue, and this GitHub issue, but to be honest I don't entirely understand what's being discussed or what it means for how I should proceed. It seems that operations on const generics used to be allowed, but the feature was killed in the pursuit of faster compile times and/or smaller libraries. (I may be misunderstanding, though!)

This forum post and this forum post are related, but do not seem to answer my question.

I suppose that I could refactor the mx, ex, and sx members out of the struct, since they're really only populated for plotting purposes. On the other hand, that seems like needless fragmentation of data structure.

Is there a workaround? Is this a planned feature? Is it worth figuring out where to put and how to name another struct to store this extremely closely related data?

If you prefer to answer on StackOverflow, see this post.

Rust Playground

#![feature(generic_const_exprs)]
#![allow(incomplete_features, unused)]

pub struct FilterStorage<const N: usize, const P: usize, const COUNT: usize>
where
    [(); 2 * COUNT]:,
{
    mx_prior: One<N, COUNT>,
    mx_post: One<N, COUNT>,
    ex_prior: One<N, COUNT>,
    ex_post: One<N, COUNT>,
    sx_prior: One<N, COUNT>,
    sx_post: One<N, COUNT>,
    mx: Two<N, COUNT>,
    ex: Two<N, COUNT>,
    sx: Two<N, COUNT>,
    ez: Three<P, COUNT>,
    sz: Three<P, COUNT>,
}

type One<const N: usize, const COUNT: usize> = [[f64; N]; COUNT];
type Two<const N: usize, const COUNT: usize> = [[f64; N]; 2 * COUNT];
type Three<const P: usize, const COUNT: usize> = [[f64; P]; COUNT];

Const computation like 2 * COUNT needs nightly feature and a well-formed bound

1 Like

Could you explain what this bound means? I have never seen anything like it.

Also, what is the reason for extracting the arrays into their own types?

Edit

I guess the bound is about restricting the value of COUNT to not be too large, such that 2*COUNT is still a valid array size. But I didn't know that you could just leave the trait bound off.

2 Likes

Cool, that makes sense.


First, the syntax Ty:, in where trait bounds is a valid and simplest trait bound in Rust:

pub fn f() where u8:, str: {} // This compiles!

so [(); 2 * COUNT]: is totally valid there as a bound.

Second, why do we need it? The last link I post above already answers: Const well-formedness and const equality - HackMD

To implement const well-formed bounds, we now use this equality relation to require every generic const to equate to a constant which is part of the public API and also propagate these bounds upwards.

What is well-formed (aka wf) means? It's up in the air for years. But it's important to know wf for rustc

A monomorphic well-formed const can be successfully evaluated, so both panic!() and loop {} are not const wf while 1 + 2 is.
Only wf expressions are useable in types. An expression which is not wf results in a compile time error.

So I guess the reason for us to write the const wf bounds is explicitly to tell:

  • the const eveluation in bounds are well-formed to be used in types and continue
  • if panics happen amid the const eveluation, the bounds are not satisfied and rustc will stop and raise the error

The history of wf on trait bounds is here in 1214-projections-lifetimes-and-wf - The Rust RFC Book . Once wf is well defined, implied bounds is easy to achieve:

So it's not impossible to make const wf bounds implied to save us from writing the redundant bounds.

1 Like

I think I'll just pull those members out of the struct until generic_const_exprs are stable. Thanks @vague!

Update: this answer to a separate question brought typenum and generic_array to my attention, which could be used to implement the desired FilterStorage struct as follows:

pub struct FilterStorage<const N: usize, const P: usize, COUNT>
where
  COUNT: ArrayLength<[f64; N]> + ArrayLength<[f64; P]>,
  U2: Mul<COUNT>,
  Prod<U2, COUNT>: ArrayLength<[f64; N]>
{
  mx_prior: GenericArray<[f64; N], COUNT>,
  mx_post: GenericArray<[f64; N], COUNT>,
  ex_prior: GenericArray<[f64; N], COUNT>,
  ex_post: GenericArray<[f64; N], COUNT>,
  sx_prior: GenericArray<[f64; N], COUNT>,
  sx_post: GenericArray<[f64; N], COUNT>,
  mx: GenericArray<[f64; N], Prod<U2, COUNT>>,
  ex: GenericArray<[f64; N], Prod<U2, COUNT>>,
  sx: GenericArray<[f64; N], Prod<U2, COUNT>>,
  ez: GenericArray<[f64; P], COUNT>,
  sz: GenericArray<[f64; P], COUNT>,
}

I regret how noisy this solution is, so hopefully the nightly feature generic_const_exprs makes its way into stable Rust soon (@vague's response shows what this might look like). Ideally, we'd also be able to get rid of the where section, but const generics expressions are hard.

1 Like

For completeness, I'd like to explain why I used Prod instead of op!:

derive cannot be used on items with type macros.

Thus, if you use !op but want Default and/or Debug, you'd have to implement them yourself.

If you instead use Prod (as I did here), you can derive those traits (so long as you add the appropriate trait bounds for Default).

1 Like

Great stuff… I wasn’t even aware of this!

On that note I had chosen the usage of op! mostly for the syntactical similarity to the code at hand in my post, anyways; looks like typenum even has a specific Double<T> type synonym which may be even nicer (which in turn is used on bitshift-by-one instead of multiply-by-two, so the trait bound would be different it it was used).

1 Like

op! is definitely nicer from a readability/similarity standpoint! I'll have to try Double<T> -- seems like a good fit for my use case. mx_prior and mx_post get zipped into mx, so that's why it has double the length, by the way. It's the same story for ex and sx.

Figuring this out has lifted a huge roadblock, so hopefully I'll be able to share some astrodynamical goodness when I finish my project in a year or so.

Edit: Cool, that wasn't so bad! (The compiler told me exactly what to put, lol)

use generic_array::{ArrayLength, GenericArray};
use std::ops::Shl;
use typenum::Double;

#[derive(Debug)]
#[derive(Default)]
struct FilterStorage<const N: usize, const P: usize, COUNT>
where
  COUNT: ArrayLength<[f64; N]> + ArrayLength<[f64; P]> + Shl<typenum::B1>,
  Double<COUNT>: ArrayLength<[f64; N]>,
  [f64; N]: Default,
  [f64; P]: Default,
{
  mx_prior: GenericArray<[f64; N], COUNT>,
  mx_post: GenericArray<[f64; N], COUNT>,
  ex_prior: GenericArray<[f64; N], COUNT>,
  ex_post: GenericArray<[f64; N], COUNT>,
  sx_prior: GenericArray<[f64; N], COUNT>,
  sx_post: GenericArray<[f64; N], COUNT>,
  mx: GenericArray<[f64; N], Double<COUNT>>,
  ex: GenericArray<[f64; N], Double<COUNT>>,
  sx: GenericArray<[f64; N], Double<COUNT>>,
  ez: GenericArray<[f64; P], COUNT>,
  sz: GenericArray<[f64; P], COUNT>,
}

fn main() {
  let filter_storage = FilterStorage::<3, 2, typenum::U1>::default();
  println!("{:#?}", filter_storage);
}

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.