Search code examples
rusttraits

How to deal with trait types?


I come from JVM world and I'm struggling with dealing with the traits. My goal is to provide an interface (that what it would be called in Java) that would represent the time provider, so I could use 'real' implementation in production environment and 'fake' in the tests, so I could stub the time.

In Java I'd have

interface Clock {
    Instant now();
}

class UtcClock implements Clock {
    Instant now() {
         return Instant.now();
    }
}

and then I could use Clock type as any other type.

In Rust I have

pub trait Clock {
    fn now(&self) -> DateTime<Utc>;
}

pub struct UtcClock;

impl Clock for UtcClock {
    fn now(&self) -> DateTime<Utc> {
        return Utc::now();
    }
}

However to be able to use dynamic type Clock in Rust and to move it between threads I have to used boxed type e.g. Arc<dyn Clock + Send + Sync> which wouldn't be required if I'd use concrete type UtcClock. Is provided solution of using traits idiomatic in Rust? Or there are other techniques to decouple the 'interface' and the implementation? If it is OK then is there any way to make it look better than Arc<dyn Clock + Send + Sync>?


Solution

  • You don't have to use a trait object (i.e. a dyn Clock of some kind), you may also be able to use regular generics:

    fn use_clock<T: Clock>(clock: T) {
      let time = clock.now();
      // ...
    }
    

    Though I imagine you'll have a struct containing various "services" and having lots of generics everywhere might get slightly messy.

    As for trait objects, if you just want to "not have to think about it" and get Java-like behaviour, an Arc<dyn Clock> will largely do what you want, other than thread-safety related traits. To resolve those issues, you can write Arc<dyn Clock + Send + Sync + 'static>, but I prefer to make these supertraits:

    trait Clock: Send + Sync + 'static /* + whatever other traits you want */ {
      // ...
    }
    

    Note, this syntax doesn't do inheritance like the equivalent would in Java. Instead it imposes the restriction that: types which implement Clock must also implement Send, etc. Especially when working with unit structs (which are Send + Sync + 'static implicitly), moving the constraints to the trait definition can help cut down on boilerplate.

    There's also type aliases, which essentially allow you to change the "spelling" of a type:

    type ArcClock = Arc<dyn Clock + Send + Sync + 'static>;
    

    and then you can use ArcClock like a regular type. Note, to the compiler, ArcClock is literally the same thing as Arc<dyn Clock + ...>.

    As an aside, trait objects are often looked down upon in Rust for "bad performance". While it is true that calling a method on a trait object is generally harder to optimize than a regular monomorphized generic and requires an extra pointer indirection, this overhead is likely fairly small unless you're calling the method in a tight loop.

    For example, in the case of a web server that stores the time a user was created, the overhead of the trait object will be negligible. FWIW, all java method calls behave in this way, so at worst you'll get Java-tier performance.