4 ms·
Your concerns are not unwarranted. I've done several Rust projects and usually there really isn't any trade-off. If you want something open for extension, use
by ragnese 6y ago
Your concerns are not unwarranted. I've done several Rust projects and usually there really isn't any trade-off.
If you want something open for extension, use a trait unless you can prove to yourself that you "can't". If it's closed or unlikely to be extended, use an enum.
Think of it this way: In Rust you have a choice between open and closed (trait vs enum). In Java, you get open (interface) and that's it. Other languages do also have both, like Rust: Swift, Kotlin, apparently Julia.
Traits have a few sharp edges because of the nature of Rust being built around zero-cost abstractions. But, most of the time, they work just like an interface in Java. The snag I most often hit is if one wants to return Self in a trait method. The most obvious way to work around that, IIRC, is to just return a Box<dyn MyTrait>. Keep in mind that in many languages, these things actually work similarly except that the language will just happily throw stuff on the heap transparently. Rust requires you to acknowledge that the former case needs to be boxed and likely on the heap, since you can't know its size at compile time.
In this particular case of extending an enum, I don't see it nearly as horrible as you're describing it. Let's look at it from the POV of another implementation in another language (I don't know Julia): You have an interface, Player. That interface defines a couple of methods. You implement three classes that conform to that interface. Now, six months later you come back and want to add a fourth. You make a new class, say that it implements Player and then your IDE/compiler yells at you until you implement those methods. In the Rust-enum version, you have an enum with three variants and a couple of methods, each of which has to match on the variants. Six months later, you want to add a fourth PlayerClass variant. So, you add it and then your compiler yells at you until you implement the fourth match branch in the couple of methods on the class.
That doesn't seem that different.
To answer some of your inner questions: Yes, Rust devs reach for enums a lot. But really only if you don't expect the cases to change much. Writing version 1.0 in Rust usually takes longer than in other languages because Rust is still a low-level language. Rust's performance advantage doesn't matter for most applications. People choose Rust for much more than the performance: its error philosophy, its enums and good matching, etc.
- thom 6y agoThis is an excellent answer, thank you. I suppose the only remaining discomfort I anticipate is if you have an internal abstraction that might one day be exposed externally in a library. That refactoring doesn’t seem fun, but I suppose these things rarely sneak up on you.
- ragnese 6y ago> I suppose the only remaining discomfort I anticipate is if you have an internal abstraction that might one day be exposed externally in a library. That refactoring doesn’t seem fun, but I suppose these things rarely sneak up on you. The place where this ends up happening most is around error handling. Crafting your public error types in Rust is an art form. Usually in Rust libraries, errors are implemented in enums. So whoever calls your API can see that it failed and then can match on the reasons it may have failed (or just wrap it in their own error type and bubble it up). If you're not careful, you can cause breaking changes in your API by doing something as simple as changing a dependency (your dep's concrete error type was wrapped inside one of your error variants). I have somewhat mixed feelings on it, but Rust does allow us to mark enums with a special annotation that forces all matches on it to include a wildcard match (even if it matches all current variants explicitly). It is generally considered good practice to mark your error enums with said annotation so that you can add failure modes in the future without requiring a major version bump for just an extra error case. One could use the same annotation for any enum, of course.