Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I specify a generic trait for operations on references to types?

I'm trying to create a trait abstracting over generic number types. Specifying that I want the trait to require T + T (i.e. T: Add<T>) is easy. I can even specify T + &T in the trait and then use that in generic functions on some type that implements that trait.

I'm running into problems however when I try to add a constraint for &T + T (i.e. for<'a> &'a Self: Add<Self>) and &T + &T to the trait. Strangely I can specify these same constraints without any issues when writing a blanket impl for the numeric trait, and also when writing a generic function for any type of that trait, but I can't get the compiler to let me just specify these constraints once on the trait instead.

Simplified example

use std::ops::Add;

trait Numeric where
    Self: Sized,
    // T + T
    Self: Add<Output = Self>,
    // T + &T
    Self: for<'a> Add<&'a Self, Output = Self>,
    // &T + T
    // should specify &T + T on this trait but instead causes compile
    // errors everywhere I try to use Numeric claiming no implementation
    // for &T + T
//  for<'a> &'a Self: Add<Self, Output = Self>,
    // &T + &T
    // should specify &T + &T on this trait but instead causes compile
    // errors everywhere I try to use Numeric claiming no implementation
    // for &T + &T
//  for<'a, 'b> &'a Self: Add<&'b Self, Output = Self>,
{}

impl <T> Numeric for T where
    // T + T
    T: Add<Output = Self>
    + Sized,
    // T + &T
    for<'a> T: Add<&'a T, Output = T>,
    // &T + T
    for<'a> &'a T: Add<T, Output = T>,
    // &T + &T
    for<'a, 'b> &'a T: Add<&'b T, Output = T>,
{}

// works
fn generic_add_0<T: Numeric>(a: T, b: T) -> T {
    a + b
}

// works
fn generic_add_1<T: Numeric>(a: T, b: &T) -> T {
    a + b
}

// doesn't infer &T + T from the trait?
fn generic_add_2<T: Numeric>(a: &T, b: T) -> T {
    a + b
}

// doesn't infer &T + &T from the trait?
fn generic_add_3<T: Numeric>(a: &T, b: &T) -> T {
    a + b
}

// works
fn generic_add_4<T: Numeric>(a: &T, b: &T) -> T
// I want to not have to specify this every time I use Numeric,
// I want Numeric to imply this constraint for me
where for<'a, 'b> &'a T: Add<&'b T, Output = T> {
    a + b
}

fn main() {
    generic_add_0(1.0, 2.0);
    generic_add_1(1.0, &2.0);
    generic_add_2(&1.0, 2.0);
    generic_add_3(&1.0, &2.0);
    generic_add_4(&1.0, &2.0);
}
like image 846
Skeletonxf Avatar asked Dec 29 '19 15:12

Skeletonxf


1 Answers

I suppose I've managed to answer my own question after staring at how num-traits did it for quite a while and trail and error.

This altered version of my initial example creates two extra traits to allow you to get very close to the original goal. Although you still need a where clause it will not increase in size as you add more constraints for other operations.

use std::ops::Add;

/**
 * Trait defining what a numeric type is, in this case just something
 * that can be added to a right hand side and yield itself as output
 */
trait NumericByValue<Rhs = Self, Output = Self>: Sized + Add<Rhs, Output = Output> {}

/**
 * All types implemeting the by value operations are NumericByValue
 */
impl <T, Rhs, Output> NumericByValue<Rhs, Output> for T where
    T: Add<Rhs, Output = Output> {}

/**
 * The trait to define &T op T and &T op &T versions for NumericByValue
 * based off the MIT/Apache 2.0 licensed code from num-traits 0.2.10
 * http://opensource.org/licenses/MIT
 * https://docs.rs/num-traits/0.2.10/src/num_traits/lib.rs.html#112
 *
 * The trick is that all types implementing this trait will be references,
 * so the first constraint expresses some &T which can be operated on with
 * some right hand side type T to yield a value of type T.
 *
 * In a similar way the second constraint expresses &T op &T -> T operations
 */
trait NumericRef<T>:
    // &T op T -> T
    NumericByValue<T, T>
    // &T op &T -> T
    + for<'a> NumericByValue<&'a T, T> {}

/**
 * All types implementing the operations from NumericByValue by reference,
 * are NumericRef<T>, ie a type like &u8 is NumericRef<u8>.
 */
impl <RefT, T> NumericRef<T> for RefT where
    RefT: NumericByValue<T, T>
    + for<'a> NumericByValue<&'a T, T> {}

/**
 * A trait extending the constraints in NumericByValue to
 * types which also support the operations with a right hand side type
 * by reference.
 *
 * When used together with NumericRef this can express all 4 by value
 * and by reference combinations for the operations using the
 * following
 *
 * ```ignore
 *  fn function_name<T: Numeric>()
 *  where for<'a> &'a T: NumericRef<T> {
 * ```
 */
trait Numeric: NumericByValue + for<'a> NumericByValue<&'a Self> {}

/**
 * All types implemeting the operations in NumericByValue with a right hand
 * side type by reference are Numeric.
 */
impl <T> Numeric for T where T: NumericByValue + for<'a> NumericByValue<&'a T> {}

fn generic_add_0<T: Numeric>(a: T, b: T) -> T
where for<'a> &'a T: NumericRef<T> {
    a + b
}

fn generic_add_1<T: Numeric>(a: T, b: &T) -> T
where for<'a> &'a T: NumericRef<T> {
    a + b
}

fn generic_add_2<T: Numeric>(a: &T, b: T) -> T
where for<'a> &'a T: NumericRef<T> {
    a + b
}

fn generic_add_3<T: Numeric>(a: &T, b: &T) -> T
where for<'a> &'a T: NumericRef<T> {
    a + b
}

fn main() {
    generic_add_0(1.0, 2.0);
    generic_add_1(1.0, &2.0);
    generic_add_2(&1.0, 2.0);
    generic_add_3(&1.0, &2.0);
}

like image 185
Skeletonxf Avatar answered Sep 30 '22 04:09

Skeletonxf