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.
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
}
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);
}
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.)
#![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 {
| ^