Combine an integer type at compile time with a dynamic type

advertisements

I am using typenum in Rust to add compile-time dimension checking to some types I am working with. I would like to combine it with a dynamic type so that an expression with mismatched dimensions would fail at compile time if given two incompatible typenum types, but compile fine and fail at runtime if one or more of the types is Dynamic. Is this possible in Rust? If so, how would I combine Unsigned and Dynamic?

extern crate typenum;
use typenum::Unsigned;
use std::marker::PhantomData;

struct Dynamic {}

// N needs to be some kind of union type of Unsigned and Dynamic, but don't know how
struct Vector<E, N: Unsigned> {
    vec: Vec<E>,
    _marker: PhantomData<(N)>,
}

impl<E, N: Unsigned> Vector<E, N> {
    fn new(vec: Vec<E>) -> Self {
        assert!(N::to_usize() == vec.len());
        Vector {
            vec: vec,
            _marker: PhantomData,
        }
    }
}

fn add<E, N: Unsigned>(vector1: &Vector<E, N>, vector2: &Vector<E, N>) {
    print!("Implement addition here")
}

fn main() {
    use typenum::{U3, U4};
    let vector3 = Vector::<usize, U3>::new(vec![1, 2, 3]);
    let vector4 = Vector::<usize, U4>::new(vec![1, 2, 3, 4]);
    // Can I make the default be Dynamic here?
    let vector4_dynamic = Vector::new(vec![1, 2, 3, 4]);

    add(&vector3, &vector4); // should fail to compile
    add(&vector3, &vector4_dynamic); // should fail at runtime
}


Specifying a default for type parameters has, sadly, still not been stabilized, so you'll need to use a nightly compiler in order for the following to work.

If you're playing with defaulted type parameters, be aware that the compiler will first try to infer the types based on usage, and only fall back to the default when there's not enough information. For example, if you were to pass a vector declared with an explicit N and a vector declared without N to add, the compiler would infer that the second vector's N must be the same as the first vector's N, instead of selecting Dynamic for the second vector's N. Therefore, if the sizes don't match, the runtime error would happen when constructing the second vector, not when adding them together.


It's possible to define multiple impl blocks for different sets of type parameters. For example, we can have an implementation of new when N: Unsigned and another when N is Dynamic.

extern crate typenum;

use std::marker::PhantomData;

use typenum::Unsigned;

struct Dynamic;

struct Vector<E, N> {
    vec: Vec<E>,
    _marker: PhantomData<N>,
}

impl<E, N: Unsigned> Vector<E, N> {
    fn new(vec: Vec<E>) -> Self {
        assert!(N::to_usize() == vec.len());
        Vector {
            vec: vec,
            _marker: PhantomData,
        }
    }
}

impl<E> Vector<E, Dynamic> {
    fn new(vec: Vec<E>) -> Self {
        Vector {
            vec: vec,
            _marker: PhantomData,
        }
    }
}

However, this approach with two impls providing a new method doesn't work well with defaulted type parameters; the compiler will complain about the ambiguity instead of inferring the default when calling new. So instead, we need to define a trait that unifies N: Unsigned and Dynamic. This trait will contain a method to help us perform the assert in new correctly depending on whether the size is fixed or dynamic.

#![feature(default_type_parameter_fallback)]

use std::marker::PhantomData;
use std::ops::Add;

use typenum::Unsigned;

struct Dynamic;

trait FixedOrDynamic {
    fn is_valid_size(value: usize) -> bool;
}

impl<T: Unsigned> FixedOrDynamic for T {
    fn is_valid_size(value: usize) -> bool {
        Self::to_usize() == value
    }
}

impl FixedOrDynamic for Dynamic {
    fn is_valid_size(_value: usize) -> bool {
        true
    }
}

struct Vector<E, N: FixedOrDynamic = Dynamic> {
    vec: Vec<E>,
    _marker: PhantomData<N>,
}

impl<E, N: FixedOrDynamic> Vector<E, N> {
    fn new(vec: Vec<E>) -> Self {
        assert!(N::is_valid_size(vec.len()));
        Vector {
            vec: vec,
            _marker: PhantomData,
        }
    }
}


In order to support add receiving a fixed and a dynamic vector, but not fixed vectors of different lengths, we need to introduce another trait. For each N: Unsigned, only N itself and Dynamic will implement the trait.

trait SameOrDynamic<N> {
    type Output: FixedOrDynamic;

    fn length_check(left_len: usize, right_len: usize) -> bool;
}

impl<N: Unsigned> SameOrDynamic<N> for N {
    type Output = N;

    fn length_check(_left_len: usize, _right_len: usize) -> bool {
        true
    }
}

impl<N: Unsigned> SameOrDynamic<Dynamic> for N {
    type Output = N;

    fn length_check(left_len: usize, right_len: usize) -> bool {
        left_len == right_len
    }
}

impl<N: Unsigned> SameOrDynamic<N> for Dynamic {
    type Output = N;

    fn length_check(left_len: usize, right_len: usize) -> bool {
        left_len == right_len
    }
}

impl SameOrDynamic<Dynamic> for Dynamic {
    type Output = Dynamic;

    fn length_check(left_len: usize, right_len: usize) -> bool {
        left_len == right_len
    }
}

fn add<E, N1, N2>(vector1: &Vector<E, N1>, vector2: &Vector<E, N2>) -> Vector<E, N2::Output>
    where N1: FixedOrDynamic,
          N2: FixedOrDynamic + SameOrDynamic<N1>,
{
    assert!(N2::length_check(vector1.vec.len(), vector2.vec.len()));
    unimplemented!()
}

If you don't actually need to support calling add with a fixed and a dynamic vector, then you can simplify this drastically:

fn add<E, N: FixedOrDynamic>(vector1: &Vector<E, N>, vector2: &Vector<E, N>) -> Vector<E, N> {
    // TODO: perform length check when N is Dynamic
    unimplemented!()
}