Reputation: 9582
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
}
Upvotes: 2
Views: 818
Reputation: 58815
You could just keep using Vec<T>
for what it does best, and use your Vector<T, N>
for checked length vectors. To accomplish that, you can define a trait for addition and implement it for different combinations of the two types of vector:
trait MyAdd<T> {
type Output;
fn add(&self, other: &T) -> Self::Output;
}
impl <T, N: Unsigned> MyAdd<Vector<T, N>> for Vector<T, N> {
type Output = Vector<T, N>;
fn add(&self, other: &Vector<T, N>) -> Self::Output {
Vector::new(/* implement addition here */)
}
}
impl <T, N: Unsigned> MyAdd<Vec<T>> for Vector<T, N> {
type Output = Vector<T, N>;
fn add(&self, other: &Vec<T>) -> Self::Output {
Vector::new(/* implement addition here */)
}
}
impl <T> MyAdd<Vec<T>> for Vec<T> {
type Output = Vec<T>;
fn add(&self, other: &Vec<T>) -> Self::Output {
Vec::new(/* implement addition here */)
}
}
impl <T, N: Unsigned> MyAdd<Vector<T, N>> for Vec<T> {
type Output = Vector<T, N>;
fn add(&self, other: &Vector<T, N>) -> Self::Output {
Vector::new(/* implement addition here */)
}
}
Now you can use it almost in the same way as you were trying to:
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]);
let vector4_dynamic = vec![1, 2, 3, 4];
vector3.add(&vector4); // Compile error!
vector3.add(&vector4_dynamic); // Runtime error on length assertion
}
You could avoid creating your own trait by using the built in std::ops::Add
, but you wouldn't be able to implement it for Vec
. The left side of the .add
would always have to be Vector<E, N>
with Vec
limited to only being in the argument. You could get around that with another Vec
"newtype" wrapper, similar to what you've done with Vector<E, T>
but without the length check and phantom type.
Upvotes: 3
Reputation: 65832
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 impl
s 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!()
}
Upvotes: 3