Overspecification of Traits in Rust

So, I recently developed the following Trait:

pub trait Currency {
    type CurrencyType: PartialEq
        + PartialOrd
        + std::ops::Add
        + std::ops::Sub
        + std::ops::Mul<f32>
        + std::ops::Mul<f64>;

    fn zero() -> Self;
}

The idea behind this trait is that it forces any type that implements the trait to also implement certain other traits. The thing is, after doing this, I find myself needing to write the following lines within implementations of generics that force their generics to implement the trait Currency, such as the following (it doesn't include all the traits that I am forcing, I now):

impl<T: Currency> Fee<T> for ShortSeller<T>
where
    T: Currency<CurrencyType = T>,
    T::CurrencyType: std::ops::Sub<Output = T::CurrencyType>
        + std::ops::Mul<f64, Output = T>
        + PartialOrd
        + Copy,

Why do I need to define those same dependencis again? It makes it a bit cumbersome to need to include double traits all the time. It is probably related to the fact that I don't need all the traits in the second code, but I would like to know why cannot we avoid the second declaration, what is left unclear.

Currency with floats? Oh jeez. :smile:

You could define

pub trait CurrencyType: PartialEq
        + PartialOrd
        + std::ops::Add
        + std::ops::Sub
        + std::ops::Mul<f32>
        + std::ops::Mul<f64> {}

And use this as trait bound instead.

1 Like

Only for percentage multiplications hehe units of the currencies are integers.

I haven’t seen that way of defining traits before, but I will try to run everything with that definition, looks more compact and clean tbh.

Note that what is you see is, technically, a bug, but when bug is more than ten years old you may as well name it a feature…

I sure do wish we had implicit bound propagation, so that for a
Foo<T: Bar>(T);
all impl blocks for it would get implicit bounds, so that you only needed to spell out impl<T> Foo<T> instead of impl<T: Bar> Foo<T>.

The sheer verbosity of manually propagating bounds everywhere is why we have silly things like:

let mut hmm: HashMap<f32, FrobnificationResult> = Default::default();

// ... elsewhere
fn<K> frobnify(m: &mut HashMap<K, Frob>, k: K, f: Frob) {
    m.insert(k, f.frobnify()); // Error?? What?? Where? Why??
    // Oh, right, having a `HashMap` doesn't mean it's usable,
    // I need to propagate `K: Hash` bound upwards everywhere...
}
1 Like

I love finding bugs in compilers on my first serious project in a new language, feels smart, without actually being it.

There's an 8-year old RFC for it: 🔬 Tracking issue for RFC 2089: Extended Implied bounds · Issue #44491 · rust-lang/rust · GitHub

It's not really a bug as making all bounds implied would turn changes which are today backwards compatible into SemVer breaking changes, thus taking significant flexibility away from crate maintainers. AFAIC it's more of a lack of feature: the ability to opt in to implied bounds.[1]

Here's an article about that.


  1. The ability actually does exist for associated types, and there are some type system gymnastics one can perform around that, but it should be an intentional feature. ↩︎

1 Like