Define `const` of generic type in a generic function

Description

There is a kind of computation-heavy functions that I would like to call, which is totally determined at compile time, and in a generic way. How should I achieve this?

Code

#![feature(const_trait_impl)]
#[const_trait]
trait MyDefault {
    fn default()->Self;  // computation heavy
}

fn f<T>()->T where T:MyDefault{
    const X:T = MyDefault::default();
    // { ... }  do some computations (supposedly at runtime) with X
    X
}
fn main() {}

Error message

error[E0401]: can't use generic parameters from outer function
   --> src/zips.rs:225:13
    |
224 | fn f<T>()->T where T:MyDefault{
    |      - type parameter from outer function
225 |     const X:T = MyDefault::default();
    |             ^ use of generic parameter from outer function

Say hello to one of my pet peeves in Rust: as far as I know, you cannot do this. You can't have consts or statics that are dependant on generic parameters.

The way I've worked around this in the past is to have the implementation of MyDefault for each type handle the memoisation. Of course, that doesn't work when you use generic implementations.

In that case, you might need to use a global table that looks something like HashMap<TypeId, Box<dyn Any>>. Each impl would construct and insert a new value if one doesn't exist, then return a clone out of the table.

1 Like

This doesn't seem like a use case for const? You could just make MyDefault a regular trait and declare a let variable.

Thank you. But let is not satisfying because every time when I call f, the costly MyDefault::default() is recomputed. I suppose this is one of the usecases of const, to reduce this kind of cost?

EDIT 1

It seems you are right. If we mark the impl as const, the recomputation will not happen.
This seems to (I have difficulty letting the compiler prints at compile time, so only a rough estimation of compile time and runtime and I am not sure if it is due to the loop getting optimized away) only run the computation once

EDIT 2

It turns out it was that the loop was optimized away.

you cannot define constants depending on generic parameters in generic functions, but you can define associated constants for generic types and generic traits. the following works:

#![feature(const_trait_impl)]

use std::marker::PhantomData;

// option 1: define constant for existing trait
#[const_trait]
trait MyDefault {
	fn default() -> Self; // computation heavy
	const X: Self;
}

// option 2: define a separate trait for constant
trait MyConst {
	const X: Self;
}

// option 3: use wrapper dummy generic struct
struct WithConst<T>(PhantomData<T>);

impl<T: ~const MyDefault> WithConst<T> {
	const X: T = T::default();
}

fn f<T: MyDefault + MyConst>() -> T
{
	// create local binding.
	// use option 1
	let X = <T as MyDefault>::X;
	// use option 2
	let X = <T as MyConst>::X;
	// use option 3
	let X = WithConst::<T>::X;
	// short syntax available when only one trait is used, e.g. `T: MyDefault`
	// let X = T::X;
	// { ... }  do some computations (supposedly at runtime) with X
	// { ... }  can also do computation directly use e.g. `T::X`
	X
}
1 Like

I don't get how it can simultaneously be const and a very expensive computation.

If it can be const-evaluated, then chances are it's simple enough that the compiler will be able to eliminate it anyway.

However, you could still use a lazy static, eg. once_cell::sync::Lazy.

1 Like

Your code doesn't compile.

   Compiling playground v0.0.1 (/playground)
error[E0015]: cannot call non-const fn `<T as MyDefault>::default` in constants
  --> src/lib.rs:21:15
   |
21 |     const X: T = T::default();
   |                  ^^^^^^^^^^^^
   |
   = note: calls in constants are limited to constant functions, tuple structs and tuple variants

well, that's to be expected for unstable features. it compiles fine on my machine, and my compiler version is:

$ rustc --version
rustc 1.73.0-nightly (8ca44ef9c 2023-07-10)

you can check the change logs to figure out which toolchain works if you wish, but I don't want to dig into that rabbit hole.

1 Like

To be sure, I try whether mere impl const Trait would be enough. It turns out it is not, but we can put the value in an associated const.

#![feature(const_trait_impl)]
/// is_prime, next_prime and BOUND are used to make a loop that cannot be optimized away by the compiler, so that we can do benchmarking
const fn is_prime(mut x:usize) ->bool{
    let mut y= 2usize;
    loop {
        if y >= x {
            return true
        } else if x.rem_euclid(y) == 0 {
            x /= y
        } else {
            y += 1
        }
    }
}
const fn next_prime(x:usize)->usize {
    let mut n = x + 1;
    loop {
        if is_prime(n) {
            return n
        } else {
            n += 1
        }
    }
}
const BOUND : usize = 1000;
macro_rules! expensive {
    ()=>{
        {
            let mut x = 1usize;
            while x < BOUND {
                x = next_prime(x)
            }
            x
        }
    }
}

#[const_trait]
trait Default {
    fn default()->Self;
}

trait PrecomputedDefault {
    const DEFAULT:Self;

}
impl const Default for usize {
    fn default() -> Self {
        expensive!()
    }
}
impl<T> PrecomputedDefault for T where T:~const Default {
    const DEFAULT: Self = Default::default();
}
fn f_let_with_const_impl<T>(i:usize) ->T where T: Default {
    let t:T = Default::default();
    t
}
fn f_const_item<T>(i:usize) ->T where T: PrecomputedDefault {
    let t:T = PrecomputedDefault::DEFAULT;
    t
}
fn baseline()->usize {
    expensive!()
}
fn f_baseline(i:usize)->usize {
    let t = baseline();
    t
}

const TEST_LOOP : usize = 5;
fn test_function<F>(f:F) where F:Fn(usize)->usize{
    let start = chrono::prelude::Utc::now().timestamp_nanos();
    (1..=TEST_LOOP).for_each(|i|{let _ = f(i);});
    let end = chrono::prelude::Utc::now().timestamp_nanos();
    println!("{:<40} {:.6?} seconds", std::any::type_name::<F>(), (end-start) as f32 / 1_000_000_000f32)
}
fn main() {
    test_function(f_let_with_const_impl);
    test_function(f_const_item);
    test_function(f_baseline);
}

Output

zips::f_let_with_const_impl<usize>       0.001383 seconds
zips::f_const_item<usize>                0.000001 seconds
zips::f_baseline                         0.001339 seconds

Since you're using nightly, what you're looking for is inline_const - The Rust Unstable Book

Then you do let x = const { <T as MyDefault>::default() }; and it's guaranteed to be calculated in CTFE, not runtime.

(This is like how a fn can't use outer generics, but you can use outer generics in a closure expression. A const item can't use outer generics, but an inline const expression can.)

4 Likes

You've clearly never seen the code I've written!

Thank you! But it seems there remains a problem:

Code

#![feature(const_trait_impl)]
#![feature(inline_const)]
#[const_trait]
trait Default {
    fn default()->Self;
}

fn f<T>()where T:~const Default {
    let t = const {<T as Default>::default()};
}
fn main() {}

Error Message

error: `~const` is not allowed here
   --> src/zips.rs:264:18
    |
264 | fn f<T>()where T:~const Default {
    |                  ^^^^^^^^^^^^^^
    |
note: this function is not `const`, so it cannot have `~const` trait bounds
   --> src/zips.rs:264:4
    |
264 | fn f<T>()where T:~const Default {
    |    ^

Interestingly, the following does compile:

#![feature(const_trait_impl)]
#![feature(inline_const)]
#[const_trait]
trait Default {
    fn default()->Self;
}

const fn g<T>()->T where T: ~const Default {
    const {<T as Default>::default()}
}
fn f<T>()->T where T:Default {
    let t = g();
    t
}

fn main() {}

which is just adding a function wrapping the const block.

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.