Trouble implementing common behavior with traits

IIUC, you want to emulate classical Object Oriented Programming patterns in Rust? I'll be honest, I don't think that would be a wise idea. I have seen some attempts in the past to kinda-sorta bend the trait system into something barely resembling inheritance in classical OOP (see also this, this, and this). But it always ends up being hacks-on-hacks, and probably nothing like what the OP really needed.

Rust favors composition over inheritance, which means that it's more natural for structs to contain other structs than it is to extend existing structs with new fields or methods. I know, I know, this is probably not what you were hoping to hear. But I learned very quickly on my first dive into Rust that doing things The Rust Way (:tm:) always works out far better in the end.

Without getting into too many details, Traits are ideal for situations where you want to share method signatures across multiple data types (generic programming). They don't allow you to add fields to existing structs; only methods! Another common use for Traits is code reuse via default implementations. Trait Objects also serve another purpose when used with Generics; duck typing! (Trait Objects are distinct from Traits)

If you haven't already, I highly recommend reading Chapter 17 of The Rust Programming Language book. It will also help if you have a good understanding of Traits and Generics, as described in Chapter 10, prior to diving into Chapter 17.

You will probably want to evaluate your options with an API that supports composition. Use Trait default implementations for code reuse, and individual Trait implementations for duck typing. Use Generics and Trait Bounds if you need methods that accept any data type (as long as it implements the required Traits). And whatever you do, don't think in terms of inheritance, base classes, abstract classes, or subclasses. :wink:

To address the 3 points you raised:

  1. This spot on; like I said, traits cannot define fields.

  2. This is true for default implementations. For individual Trait implementations, the methods can access any field on the struct. So e.g. the getter/setter pattern works. Trait default implementations can also call Trait implementations, like getters and setters!

  3. Another correct observation; how else would you impl CursorTrait for Cursor if you don't import CursorTrait? And again, it sounds like you're only using default implementations. Which of course have their uses, but are obviously limited. The typical use case for Traits (in my experience) has been to provide unique behavior for Trait methods on each new data type. In this case, the Trait definition only declares the method signature, which still has to be imported, and then the Trait is implemented for the data type in the same module that defines the struct.

Finally, I hope this has been some help. Don't take it personally that I'm trying to steer you away from classical OOP (it's fine if you're using Java or C#!) The suggestions are only meant to help point you in the right direction. The programming paradigms available in Rust are a bit different from other languages. But like I mentioned briefly before, it's actually a good thing because tools like the borrow checker and type checker will save you from making mistakes that are common to paradigms that Rust doesn't really intend to support.

2 Likes