Generic Numeric Computations In Rust

Generic Numeric Computations in Rust

The Problem

Implement moderately complex operations with traits (generics)

fn double<T: Number>(value: T) -> T{
value+value}

Number is a trait that needs to be defined. It needs to support addition with another Number and produce a new Number:

trait Number: Add<Self, Output=Self> {}

Because fn double uses value twice, it needs to implement Copy. For f32 and f64 have copy so its ok for now.

Type also needs to be Sized - meaning the size of values is known at compile time. This is because we need to store result of function, and that result must be sized.

Trait becomes:

trait Number: Sized + Copy + Add<Self, Output=Self> {}

This should be complete for working with f32 and f64 data types.

A Basic Implementation

use core::ops::Add;
trait Number: Sized + Copy + Add<Self, Output=Self> {}

impl Number for f64{}
impl Number for f32{}

fn double<T: Number>(value: T) -> T {
	value+value
}

pub fn main() {
	double(4.09_f32);
	double(5.01_f64);
}

Adding more arithmetic

To add support for the other operations need to add the traits for multiplication, etc. New trait:

trait Number: Sized + Copy + ADD<Self, Output=Self> + Mul<Self, Output=Self>

Now this can do basic arithmetic operations (+,-,*,/) BUT if we try:

fn circle_circumference<T: Number>(r: T) -> {
	2.0 * r * PI
}

This will not work because we cannot use float constants in our T as it is a Number not an f64/32

Working with Constants

Cannot directly define literals for the Number and need to convert from existing numeric types, by implementing From<T> for a type we can create literals for. So will need to define PI for our Number:

use std::f64;
trait Number: From<f64> + /* Other required traits */ {
	const PI: Self,
}

impl Number for f64 {
	const PI: Self = std::f64::consts::PI;
}

impl Number for f32 {
	const PI: Self = std::f32::consts::PI;
}

Can go further and macro for implementing constants for Number:

macro_rules! impl_number_consts {
	($ty:ty, $($const_name:ident), *) => {
		$(const $const_name: Self = std::$ty::consts::$const_name;)*
	};
}

impl Number for f64 {
	impl_number_consts!(f64, PI, E, SQRT_2);
}

complete working example:

use std::ops::*;

trait Number:
    Sized +
    Copy +
    Add<Self, Output=Self> +
    Sub<Self, Output=Self> +
    Mul<Self, Output=Self> +
    Div<Self, Output=Self> +
    From<f64> {

    const PI: Self;

}

impl Number for f64 {
    const PI: Self = std::f64::consts::PI;
}

impl Number for f32 {
    const PI: Self = std::f32::consts::PI;
}

fn circle_circumference<T: Number>(r: T) -> T {
    T::from(2.0) * r * T::PI
}


Adding Comparisons

We still can't compare our generic types, and can be used by adding hte std traits needed:

	trait Number: Sized + Copy + Add<Self, Output=Self> + Mul<Self, Output=Self> + Sub<Self, Output=Self> + Div<Self, Output=Self> + PartialEq + PartialOrd {}

Mathematical Functions

Still need to add support for stuff like sin and exp for the Number generic. Can use macros to help some but there will be a lot of boilerplate to make this generic work:

trait Number ...
    fn sin(self) -> Self;
    fn exp(self) -> Self;
    fn powf(self, other: Self) -> Self;
    // ... many more methods
}

and now should be able to write:

   fn fancy_maths<T: Number>(a: T, b: T) -> T {
       ( a.powf(T::from(3.0)) + b.powf(T::from(4.0)))
   }

Using the num_traits Crate

Could use the crate for this instead of writing our own generic completely by hand.

num_trats example:

use num_traits::Float;

fn double<T: Float>(value: T) -> T {
    value + value
}

fn circle_circumference<T: Float>(r: T) -> T {
    T::from(2.0).unwrap() * r * T::PI()
}

fn fancy_maths<T: Float>(a: T, b: T) -> T {
    a.powf(T::from(3.0).unwrap()) + b.powf(T::from(4.0).unwrap())
}

pub fn main() {
    println!("{}", double(4.09_f32));
    println!("{}", circle_circumference(2.0_f64));
    println!("{}", fancy_maths(2.0_f32, 3.0_f32));
}

Conclusion and Next Steps

After this can extend generic to work on arrays of numbers, ndarray nalgebra and more.