Foreign trait restrictions on native types make generics hard to use

Traits seem to have been created contemplating a use case where they are implemented as needed for specific types. There is one kind of case where this pattern is vexing, mathematical types.

For example, many crates end up implementing their own 3D and 2D vector types because there is no native vector algebra library in rust. This leads to developers having to convert between types across library uses even when the vast majority of the time the types are equivalent both in terms of byte layout and in terms of operations.

A way around this is to try to make blanket implementations, but these then start clashing with foreign trait constraints. I will give an example that I am struggling with.

I already have a trait that abstracts away Vector types, it works, I am using it. But for long reasons to explain I now need it to work with scalar types, but for it to work I need to be able to index numbers. Consider this snippet that's trying to do just that:

trait Indexable {
    type Item;
    fn at(&self, i: usize) -> &Self::Item;
    fn at_mut(&mut self, _i: usize) -> &mut Self::Item;
}

impl Indexable for f32 {
    type Item = f32;

    fn at(&self, _i: usize) -> &Self::Item {
        self
    }

    fn at_mut(&mut self, _i: usize) -> &mut Self::Item {
        &mut *self
    }
}

impl<T, I> Indexable for T
where T: Index<usize, Output = I> + IndexMut<usize, Output = I>
{
    type Item = I;

    fn at(&self, i: usize) -> &Self::Item {
        &self[i]
    }

    fn at_mut(&mut self, i: usize) -> &mut Self::Item {
        &mut self[i]
    }
}

This won't compile because f32 and the index traits are foreign and so it could technically be that one day the index trait is implemented for f32. However, it is not currently implemented for it and this is precisely trying to unify behaviour between indexable objects like vectors and slices and scalars, which is what I need.

The only alternative would be to make a wrapping type, but if I do a wrapping type, then it automatically requires the user to do explicit casts, but the entire point of what I am doing is to avoid having to do casts between types that are algebraically equivalent.

I feel stuck here.

I'd love to hear that explanation. Because that sounds like a design flaw and/or an xy-problem.

The explanation is that I am doing a numerical decomposition of an implicit function into smaller, local functions.

Right now the logic acts on a function of the form f(x, y, z) = w. On the current logic, the system is abstract to the underlying scalar.

Now say you want this to work, generically over any vector function. i.e. you want it to also work for example over f(x, y, z) = (w, u, v). And you want it to remain agnostic over the specific scalar type the vector space is represented over.

If you make a new type to wrap around f32 and f64 to make them act as one dimensional vectors, you are forcing explicit conversion at the call site, but this is quite silly, since the system can already work with f32 and f64 without such conversion.

You could make a copy of the existing structure then modify it to deal with vector types, but then you have redundant code.

The cleanest and most ergonomic implementation is one that simply acknowledges that scalar fields are vector fields, and thus the method can be implemented abstractly as a function from a vector field into a different vector field (which will include scalar fields).

I'm not in your shoes, but from my perspective it feels a little confusing to treat scalars as vectors. The explicit use of one-dimension vector would be more coherent and wouldn't hide some behaviour.

For the general issue of blanket implementations using external traits in the bounds, sometimes I wonder if those headaches couldn't be solved by allowing specific implementations to overwrite the general blanket implementation if they're in the same crate, as you did above. It would imply that the specific implementation prevails if Index is implemented for f32 one day. There must be cases that make it unsafe or impractical, I suppose, and it would likely open a can of worms when deciding what's more specific and what's more general.

Another workaround is to implement everything manually, possibly using a declarative macro to avoid repeating code, like in the standard library (something I don't like to see because of its lack of clarity). However, here it would force the user of an external type to implement the trait where the type is defined, which isn't possible if they don't own that crate; there, a wrapper must be used.

What do you mean by "the system can already work with f32" when you're saying it doesn't compile?

The typical way to do this is to impl From<f32> for Vector<f32, 1>, just like there is impl From<u32> for u64.