Trouble implementing common behavior with traits

As my first rust project, I have a simple program I'm converting from python to rust. It uses a library that's an interface to an obscure database engine.

I have 3 similar resources that are giving me trouble: Table, Cursor and Statement.

A Table is almost interchangeable with a Cursor, but there are some special operations that only apply to a Table.

In python I subclassed Table from Cursor and implemented 2 mixins: CursorStatementMixin and TableStatementMixin, to implement functions that apply to those combinations.

In rust since I can't subclass I figured I'd implement the following traits: CursorTrait, CursorStatementTrait and TableStatementTrait.

Cursor will then impl CursorTrait and CursorStatementTrait.

Table will impl CursorTrait and TableStatementTrait and it's own unique functions.

Statement will impl CursorStatementTrait and TableStatementTrait and it's own unique functions.

Here's the problems I'm having with this approach:

  1. I can't figure out how to define data members in a trait. This is NOT that big a deal, but I'm mentioning it for completeness. I have to add the following to each of the three structs:

handle: ADSHANDLE; // type ADSHANDLE = usize;

  1. I can't access data members of self. I have a common init() function that needs to run for both Cursor and Table to cache a list of the fields in the table. Since this is a private function, I could obviously move this to a generic, but there will be other functions that need to be public and need to access fields inside the Cursor/Table.

error[E0609]: no field fields on type &mut Self
--> src\ace.rs:127:8
|
127 | self.fields = Vec::new();
| ^^^^^^

  1. I have to explicitly import traits in user code for the methods to be visible. This just feels ugly. I get that I would need to do this if I wanted to use them in generics, but I feel that I shouldn't have to just to call methods on the object that are defined by the trait.

use ace::CursorTrait;

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

I do appreciate the feedback greatly! The reason I posted here is because I want to learn the proper rusty way to do this.

I have read those chapters carefully and I can’t figure out how to structure this so that I can avoid copy/pasting a whole bunch of crap.

So... in to trying to process what you’re saying...

Composition over inheritance:

I could implement a Handle struct that implements all the common code that deals with handle management. Then have other objects contain a Handle. However that leaves me with a couple problems:

Closing a Table or Cursor will involve either 1) the user has to type cursor.handle.Close() (for example ), or 2) I have to copy/paste any handle management functions. Admittedly there aren’t many so this isn’t that big of a deal, but neither of these solutions are satisfactory on a larger scale.

There also aren’t that many functions that overlap between the Statement object and the Cursor / Table objects.

However, the overlap between Cursor and Table is very great. This is where I really need to try to avoid the code duplication. Cursor has many functions and they are all applicable to a Table. Table has only a handful of additional functions. I really don’t want users to have to type table.cursor.MoveNext() and I don’t want to implement wrapper functions for all the Cursor functions applicable to a Table. All of these are code duplications. If I use composition solely then closing a table object prematurely would look like: table.cursor.handle.Close(). This is ugly and harder for the user to remember where a function lives and makes the code harder to read. Also, Table objects are far more common than Cursor objects in all the code I’ve written to date due to the way I have to interact with these databases. ( This database doesn’t support running SQL against encrypted databases! )

So, I could implement the Cursor functions as a Generic that both Cursor and Table implement. I’m okay with doing this, but it doesn’t solve the problem with the function overlap with Statement because you can’t (I think?) implement more than one Generic on a single object. If I could, that would be a perfect solution avoiding any code duplication.

I haven’t studied macros much, and I don’t want to, but could I define macros that implement the common function methods? They would be absolutely huge monsters and while I used them very effectively myself in C/C++, I have had many bad experiences trying to understand other people’s code and code readability is very important to me.

Hmm, I just had an idea. I can’t test it right now so I’m writing this so that I hopefully won’t forget. Maybe I could implement generics for each category of shared functions and then implement my structs like this:

struct Handle {

handle : ADSHANDLE

}

struct TableBehavior {

table_name : String,

....

}

impl Connection {

fn new_table ( ... ) {

...

return TableBehavior<CursorBehavior> {

handle: handle,

table_name: table_name,

}

}

}

So, what I’m trying to say in a long round-about way is that I am really trying to figure out the rusty way to do this without code duplication or creating a hassle for the users of the library.

I think using macros will be the easiest solution for you right now, although ideally you should consider rethinking your API (see "XY problem"), but it will require more familiarity and experience with Rust.

You can write the following macros:

struct Cursor;
struct Table;
struct Statement;

macro_rules! impl_cursor {
    ($tp:ty) => {
        impl $tp {
            // your cursor code
        }
    }
}

macro_rules! impl_statement {
    ($tp:ty) => {
        impl $tp {
            // your statement code
        }
    }
}

impl_cursor!(Cursor);
impl_cursor!(Table);
impl_statement!(Cursor);
impl_statement!(Table);
impl_statement!(Statement);

(BTW please use ``` to format your code, otherwise it's hard to read)

Yeah, I don't like this behaviour that much myself. There is good reasons for it in general, but quite often it gets into the way. This RFC#2309 aims to fix it.

So yeah generic chaining was a bust. A generic appears to have to contain it's type since you can't subclass.

I did some research on "XY Problem" as this is a new term to me, and I don't believe it's applicable, let me explain:

I'm interfacing with a 3rd-party DLL. I need to make sure that resources I allocate from that DLL get released properly. Some of these resources shared API functions, but the functions to acquire/release them are different.

Oh, and I hate duplicating code. It causes so many problems. I really am trying to figure out the proper rust solution to this.

I'm attempting your macro suggestion, but I'm having a problem with lifetimes. This:

error[E0261]: use of undeclared lifetime name 'a
--> src\ace.rs:472:21

472 | impl_cursor!(Cursor<'a>);

^^ undeclared lifetime

I found several things on google talking about implementing lifetime support in macros, but I can't seem to find any examples of the syntax that will make this work.

Thanks for your continued attention.

Royce Mitchell, IT Consultant
ITAS Solutions

royce3@itas-solutions.com

newpavlov

    February 27

I think using macros will be the easiest solution for you right now, although ideally you should consider rethinking your API (see “XY problem”), but it will require more familiarity and experience with Rust.

You can write the following macros:

> 
> struct Cursor;
> struct Table;
> struct Statement;
> macro_rules! impl_cursor {
> ($tp:ty) => {
> impl $tp {
> // your cursor code
> }
> }
> }
> macro_rules! impl_statement {
> ($tp:ty) => {
> impl $tp {
> // your statement code
> }
> }
> }
> impl_cursor!(Cursor);
> impl_cursor!(Table);
> impl_statement!(Cursor);
> impl_statement!(Table);
> impl_statement!(Statement);

For Cursor<'a> you need to write impl<'a> Cursor<'a> { .. }, thus the macro error you've got. If all three types contain one lifetime 'a, then you can just write impl<'a> $tp { .. } in the macros. It's a bit hacky, but it will work. If you want more general solution you'll need to list type lifetimes as a separate macro argument(s) using $lifetime:tt.

P.S.: Note that with this approach you will not be able to write generic code over Cursor or Statement functionality, for it you'll still need traits (with getter/setters if needed).

FWIW, lifetimes have also plagued me in the past. I usually fix it by avoiding storing borrowed references entirely. Which means using RAII patterns, or (rarely) boxing items on the heap with Rc<T> / RefCell<T> and friends. If you're in a position where you cannot avoid storing borrows, all I can say is godspeed!

This is one of the reasons I haven't used FFI with Rust. Common paradigms in other languages don't always have a nice Rusty equivalent.

I got it working with macros. I was missing the <'a> on the impl inside the macro. The error message confused me because it kept pointing at the macro invocation instead of the definition.

I want to thank both of you for your awesome assistance.

It's really rough when learning a new language, and the learning curve for rust seems a bit steeper than most, but it's obvious that the language was designed by people who are sick of compilers not catching things it should be able to.

I'm very happy that rust is mostly pretty good about giving me line numbers inside a macro. This alone made dealing with macros in C/C++ a nightmare. I only say mostly because of the issue I mentioned above, but this is likely the nature of the beast - I've written hand-rolled parsers and interpreters so I know the kind of holes you can end up in and I still don't understand half of what I think I'm telling rust to do.

I especially like the fact that you can't hide the fact that you're using macros.

There are sooo many things that rust does right that require work-arounds in other languages.

I don't think I'll ever be able to divorce myself from Python, I have over 100k loc I'm maintaining ( and I only started programming in Python 3 or 4 years ago! ), but I think I may have a brand new favorite programming language!

Thank you both and as well as the people that invented rust! Thank you, thank you, thank you!

Now, I think there might still be room for improvement, but as a brand new user I'm in no position yet to make that judgement, so take the rest of this with a grain of salt and please don't construe any of it as a complaint....

I'd like to see a fix so that indirect use of traits don't have to be used. I think this would have prevented the need for macros.

I know many are vehemently against this, but I'd also really like to see inheritance as an option if it can be done without compromising the lifetime guarantees. There are some design patterns that are definitely most easily expressed with inheritance, and being forced to resort to macros to meet all the criteria for this relatively simple module was disappointing. Also being forced to stop and come up with a work-around because the most appropriate design pattern isn't possible in your choice of language really throws a wet blanket on productivity. Right now I'm chalking it up to learning pains and it's my hope that as I retrain my brain to think in rust terms this complaint will disappear, but if it doesn't, it will become a sore spot for rust. Productivity was the reason I walked away from C/C++ after 15 years of use.

1 Like