Generics, I thought I knew

I use rust for fun. Mostly making patterns to watch. While practicing my shader foo, I decided to try and port some GL shaping functions to Rust.
Easy enough, just weak some function names (pow, powf, etc) and calling conventions dot notation, etc.

Yeah, easy. But...in shaderland, I mostly use "float" to declare floating variables and define the precision with a GL compiler directive. Something like...

#ifdef GL_ES
precision mediump float;
#endif

<<< SNIP >>>
float doubleCubicSeat(float x, float a, float b)  {
    float epsilon = 0.00001;
    float a_min = 0.0 + epsilon;
    float a_max = 1.0 - epsilon;
    float b_min = 0.0;
    float b_max = 1.0;

    a = min(a_max,max(a, a_min));
    b = min(b_max,max(b, b_min));

    float y = b - b * pow((1.0 - x / a), 3.0);
    y = y * ( 1.0 - step(a, x));
    y = y + step(a, x) * ( b + (1.0 - b) * (pow(((x - a) / ( 1.0 - a )),3.0)));

    return y;
}

In Rust this comes out to something like this.
NOTE : This math is correct, I checked it myself. Of course this is the internet and someone will point out some obvious math flaw.

pub fn double_cubic_seat(x: f32, mut a:f32 , mut b:f32 ) -> f32 {
    let epsilon: f32 = 0.00001;
    let a_min: f32 = 0.0 + epsilon;
    let a_max: f32 = 1.0 - epsilon;
    let b_min: f32 = 0.0;
    let b_max: f32 = 1.0;
    a = a_max.min(a_min.max(a));
    b = b_max.min(b_min.max(b));

    if x <= a {
        b - b * (1.0 - x / a).powi(3)
    } else {
        b + (1.0 - b) * (((x - a) / 1.0 - a).powi(3))
    }
}

And it appears to work and the slopes look the same on my tests. But I want to make it generic.

So I put it a <T>

pub fn double_cubic_seat<T>(x: T, mut a: T, mut b: T) -> T 

that led me down a maze of compile errors. I tried for a while, adding trait bounds. std::ops::Sub<Output = T> and such but each compiler suggestion seamed to make the errors longer. I tried using T instead of f32 on my numbers but even that with the "from_f32" not existing for my T. Is the rust way to write a macro instead

1 Like

use num-traits crate, you would get something like this:

use num_traits::Float;

pub fn double_cubic_seat<T: Float>(x: T, mut a: T, mut b: T) -> T {
    let epsilon = T::from(0.00001).unwrap();
    let zero = T::zero();
    let one = T::one();

    let a_min = zero + epsilon;
    let a_max = one - epsilon;

    a = a_max.min(a_min.max(a));
    b = one.min(zero.max(b));

    if x <= a {
        b - b * (one - x / a).powi(3)
    } else {
        b + (one - b) * ((x - a) / (one - a)).powi(3)
    }
}
4 Likes

In Rust your code has to work for every generic T that meets the stated bounds, so you can only make use of capabilities reflected in the bounds. (Numerics tend to require many bounds to be useful.)

If you want something more like a template, which doesn't have the bounds requirement and "merely" errors when you feed it a type that doesn't make sense, macros are what we have so far.

1 Like

Thanks. I had seen mention of that here before. I was kind of trying for "0" zero dependencies, but what the heck. The gui framework I use has it as a dependency of a dependency anyway. Their documentation is not too good. No good examples. But then again, it is so easy to use documentation isn't needed?
Looking at their code I see generics on steroids.

1 Like

Yes, that is what I "discovered" independently. Thanks for the clarity.