Search code examples
enumsrustpolymorphismtraitssubtype

What is the favored alternative to subtyping that library writers choose, and why?


I have two types, A and B, which share a common interface T. I want to write functions which work with both A and B through the interface T.

In C++, I would have T be an abstract superclass of both A and B, and write functions accepting a parameter of type T.

In Rust, it seems like I have a good two good solutions.

Enums

enum T { 
    A { /* ... */ },
    B { /* ... */ },
}

impl T {
   fn method(&self) {
       match *self {
           // ... 
       }
   } 
}

In the methods of T, I can match on the variant to specialize the type, and then write whatever I want while accesses variant members. I can then write functions which accept parameters of type T directly:

fn f(t: T) {
    // I can use t.method here
}

Traits

trait T {
    // ...
}

struct A { /* ... */ }
struct B { /* ... */ }

impl T for A {
   fn method(&self) {
      // ...  
   } 
}

impl T for B {
   fn method(&self) {
      // ...    
   }
}

With traits, the type is already specialized in the method definitions. However, to use T.method, I need to write a generic function:

fn f<X: T>(t: X) {
    // I can use t.method here
}

What bothers me here is that while both of these solutions work perfectly, the implementation is exposed: the signature of f is different in both examples. If I write a library with enums, but eventually decide what I really wanted was traits, every user-level type signature needs to be changed!

Given this fact, what choice do library writers choose, and why?

Note that I don't care about inheritance in particular, I'm not looking to import C++ idioms into Rust, I'm trying to understand what the better alternative is.


Solution

  • Do library writers favor using enums or traits?

    Do builders favor using nails or screws?

    Do doctors favor using glue, stitches, or staples?

    This is a nonsensical dichotomy. Both capabilities exist and both are used. People use the right tool for the specific job at hand.

    Start by going back and re-reading The Rust Programming Language chapter Is Rust an Object-Oriented Programming Language?.

    In short, traits allow for unbounded polymorphism, while enums are strictly bounded. Really, that's the main difference. Review what your problem domain needs and use the right thing.

    the implementation is exposed: the signature of f is different in both examples

    Yes, designing software sometimes requires some amount of upfront thought, care and design.

    If you expose an enum to your users and then add, remove or modify a variant, every usage of that enum needs to be updated.

    If you expose a trait to your users and then add, remove or modify a method, every usage of that trait needs to be updated.

    There's nothing special about the fact that you are choosing between an enum or a trait that causes this problem. You could also rename the method, add, remove or reorder your parameters. There are a lot of ways to cause trouble for your users.

    If you change your API, your users will be affected.

    I need to write a generic function

    You don't need to, but you likely should default to it for performance reasons. You could use a trait object like fn f(t: &T) or fn f(t: Box<T>) if the <T> is causing consternation.

    write a library with enums, but eventually decide what I really wanted was traits

    Now that you know you don't have to always pick one or the other, also realize you can use them together:

    enum Pet {
        Cat,
        Dog,
        Other(Box<Animal>),
    }
    
    trait Animal {}
    
    trait TimeSource {
        fn now() -> u64;
    }
    
    enum Builtin {
        Ntp,
        Sundial,
    }
    
    impl TimeSource for Builtin {
        // ...
    }
    

    Personal opinion-wise, if I'm still prototyping my code and it's not clear which choice is better, I'll probably default to using a trait. The unbounded polymorphism fits better with my preferences of dependency injection and testing style.