How do I implement/approximate inheritance?

I'm building a code generator that takes an IDL and generates Rust. One of the concepts in this IDL is that of a service (think RPC interface). Given a service definition I have to generate both the client and the server stubs. Now, there's an additional wrinkle: a service can extend from another service, which can itself extend from another service, and so on. The user should be able to call any method from the final service, as well as from its ancestors.

service ServiceOne {
  void method_one(string foo);
}

service ServiceTwo extends ServiceOne {
  void method_two(string bar);
}

service ServiceFinal extends ServiceTwo {
  void method_final(string baz);
}

In an OO language this is simple: my auto-generated code would simply indicate that ServiceFinal extends from ServiceTwo and I'd be done; the client/server stubs would have all the methods in ServiceOne, ServiceTwo and ServiceFinal available. I can't do that with Rust, so I'd like to know: what are my options?

Two things:

  • I'd like not to code-gen all the methods for the base service(s)
  • No, I cannot change the IDL

What about something like this:

trait ServiceOne {
  fn method_one(foo: String);
}

trait ServiceTwo : ServiceOne {
  fn method_two(bar: String);
}

struct ServiceImpl: ServiceTwo;

impl ServiceOne for ServiceImpl { ... }

impl ServiceTwo for ServiceImpl { ... }

I'll have to regen the code for ServiceImpl then :frowning: I'd like to avoid that because it'll get messy - especially if the inheritance hierarchy is deep.

I'll have to regen the code for ServiceImpl then

why is that? I think you should be able to generate exactly one impl for each method.

trait One { fn one(&self); }
trait Two: One { fn two(&self); }

struct Impl;

impl One for Impl {
  fn one(&self) {}
}

impl Two for Impl {
  fn two(&self) {}
}
1 Like

Also, I think this abomination would have a correct big O of code if you want to generate combinations of services.

2 Likes

You can, but you shouldn't. As you note, the way you would typically do this in a traditional OO language is to inherit functionality and possibly override methods to customize the functionality. Rust, however, is not an OO language. From what I have seen, I would characterize it as a pragmatically functional language: the emphasis is on writing functions, though not necessarily "pure" like Haskell.

This sort of thing should serve as a "code smell" that you're probably doing something wrong. If you absolutely must "inherit" a struct's implementation of a trait, you could use encapsulation and re-implement the trait on the container struct, in order to simply dispatch calls to the inner object that serves as the "superclass."

@matklad Ah. I think I understand where we're getting our wires crossed.

Consider the following example:

file_1.idl:

service ServiceOne {
  void method_one(string foo);
}

file_2.idl:

service ServiceTwo extends ServiceOne {
  void method_two(string bar);
}

file_3.idl:

service ServiceFinal extends ServiceTwo {
  void method_final(string baz);
}

Basically, these services should be independently usable. In other words, I have to generate a ServiceImpl for each of ServiceOne, ServiceTwo and ServiceFinal. Since these are three different IDL files these ServiceImpl structs would be generated in different compiler passes. I'd like to know if (when it comes to file_3.idl) I can avoid the impl ServiceOne for ServiceImpl and impl ServiceTwo for ServiceImpl. It's not exactly trivial to hoist everything up to the top level because I have to recurse down and figure out the full set of modules and types I have to pub use in the module being generated for file_3.idl.

Can the delegating solution from my second response help?

That way, in file_3.idl you'll need to know only ServiceTwo itself, and not its methods:

struct ServiceFinalImpl(ServiceTwoImpl);

impl HasServiceTwo for ServiceFinalImpl {
  type T = ServiceTwoImpl
  fn two(&self) -> ServiceTwoImpl { &self.0 }
}

impl ServiceFinal for ServiceFInalImpl {
   // only final's impls
}

@matklad Sorry! - I totally blanked on that. Taking a look and, wow - that's kinda mind-bending. Yes, definitely looks like it could work. Let me try understand it better and try it in the generated code.