# Rust Inherited
I was working on a plugin system the other day and ran into an interesting problem. Imagine you're building a distributed map/reduce framework. The core operations -- , , and -- are conceptually the same, but each function is executed as a separate binary by the platform. This binary reads incoming data, transforms it into a , does its thing, and then returns a for serialization and further processing.
Most of the logic is shared: serialization, deserialization, protocol handling, maybe even some keepalives. But there are also unique requirements for each function—for example, needs to maintain an accumulator.
From the start, I had a few goals in mind:
- The SDK should abstract away all protocol handling, exposing only the core data processing functionality.
- The setup should be minimal.
- The design should be self-explanatory.
- It should be foolproof -- each plugin type (, , ) should be mutually exclusive.
## The OOP Approach
If I were doing this in Python or C++, I'd reach for inheritance. I'd create a base class with most of the functionality and subclass it for , , and . Something like this:
|
class Plugin: |
|
def process(self): |
|
self.protocol_handshake() |
|
input = self.receive_records() |
|
output = self.handle_records(input) |
|
if output: |
|
self.send_records(output) |
|
|
|
def handle_records(self, input): |
|
raise NotImplementedError |
|
|
|
class Filter(Plugin):) |
|
def handle_records(self, input): |
|
return self.filter(input) |
|
|
|
def filter(self, input): |
|
raise NotImplementedError |
|
|
|
class Map(Plugin): |
|
def handle_records(self, input): |
|
return self.map(input) |
|
|
|
def map(self, input): |
|
raise NotImplementedError |
A developer using this SDK would just subclass one of these:
|
from sdk import Filter |
|
|
|
class MyGreenGrassFilter(Filter): |
|
def filter(self, input): |
|
return None if 'failed' in input else input |
This is clean, easy to understand, and exactly what I’d want from a developer’s perspective. The only issue? There’s no real way to enforce that a plugin is only a , , or . Someone could mix them up, and things could get messy.
## Rust Doesn’t Do That
Rust doesn’t have inheritance, nor does it let you implement traits for traits, like this:
|
trait Plugin { |
|
fn handle_records(&self, input: Vec<String>) -> Vec<String>; |
|
} |
|
|
|
trait Filter { |
|
fn filter(&self, input: Vec<String>) -> Vec<String>; |
|
} |
|
|
|
impl Plugin for Filter { |
|
fn handle_records(&self, input: Vec<String>) -> Vec<String> { |
|
self.filter(input) |
|
} |
|
} |
That just doesn’t work. Rust traits don’t stack like that.
I tried getting around this with generics:
|
trait Plugin { |
|
fn handle_records(&self, input: Vec<String>) -> Vec<String>; |
|
} |
|
|
|
trait Filter { |
|
fn filter(&self, input: Vec<String>) -> Vec<String>; |
|
} |
|
|
|
impl<X> Plugin for X |
|
where |
|
X: Filter |
|
{ |
|
fn handle_records(&self, input: Vec<String>) -> Vec<String> { |
|
self.filter(input) |
|
} |
|
} |
That worked... until I needed another command:
|
trait Map { |
|
fn map(&self, input: Vec<String>) -> Vec<String>; |
|
} |
|
|
|
impl<X> Plugin for X |
|
where |
|
X: Map |
|
{ |
|
fn handle_records(&self, input: Vec<String>) -> Vec<String> { |
|
self.map(input) |
|
} |
|
} |
Suddenly, Rust yelled at me:
|
sdk on main [?] is 🦀 v1.86.0-nightly took 12s |
|
❯ cargo build |
|
Compiling sdk v0.1.0 (/Users/angelo/sdk) |
|
error[E0119]: conflicting implementations of trait `Plugin` |
|
--> src/main.rs:22:1 |
|
| |
|
9 | / impl<X> Plugin for X |
|
10 | | where |
|
11 | | X: Filter |
|
| |___________- first implementation here |
|
... |
|
22 | / impl<X> Plugin for X |
|
23 | | where |
|
24 | | X: Map |
|
| |________^ conflicting implementation |
|
|
|
For more information about this error, try `rustc --explain E0119`. |
|
error: could not compile `sdk` (bin "sdk") due to 1 previous error |
Rust was absolutely correct -- I couldn’t guarantee that and wouldn't be implemented on the same type. And Rust really cares about safety.
## The Solution: Enum-based Plugins
After a bit of soul-searching, I landed on this approach:
|
type Records = Vec<String>; |
|
pub enum PluginType { |
|
Filter(Box<dyn Filter>), |
|
Map(Box<dyn Map>), |
|
} |
|
|
|
pub trait Plugin { |
|
fn handle_records(&self, input: &Records) -> Result<Records>; |
|
|
|
fn run() { |
|
//... |
|
} |
|
} |
|
|
|
impl Plugin for PluginType { |
|
fn handle_records(&self, input: &Records) -> Result<Records> { |
|
match self { |
|
PluginType::Filter(v) => v.filter(input), |
|
PluginType::Map(v) => v.map(records), |
|
} |
|
} |
|
} |
|
|
|
pub trait Filter { |
|
fn filter(&self, input: &Records) -> Records; |
|
} |
|
|
|
pub trait Map { |
|
fn map(&self, input: &Records) -> Records; |
|
} |
|
|
|
|
|
pub fn filter_plugin<F>(plugin: F) -> PluginType |
|
where |
|
F: Filter + 'static |
|
{ |
|
PluginType::Filter(Box::new(plugin)) |
|
} |
|
|
|
pub fn map_plugin<M>(plugin: M) -> PluginType |
|
where |
|
M: Map + 'static |
|
{ |
|
PluginType::Map(Box::new(plugin)) |
|
} |
And for the developer, it looks just as clean as the Python version:
|
use sdk::{Filter, filter_plugin}; |
|
|
|
struct Passthrough; |
|
impl Filter for Passthrough { |
|
fn filter(&self, input: Vec<String>) -> Vec<String> { |
|
input |
|
} |
|
} |
|
|
|
fn main() { |
|
const plugin = filter_plugin(Passthrough); |
|
plugin.run(); |
|
} |
## The Best Part?
Not only does this keep the API as clean as the OOP version, but it also ensures mutual exclusivity between plugin types! A type cannot be both a Filter and a Map -- Rust enforces this at compile time.
So while Rust might not have traditional inheritance, with a little creativity (and a few enums), you can still get a clean and safe design.