Make Rust Object Oriented with the dual-trait pattern
Tags: rust, architecture, howto, programming
Published: 2024-07-09 05:34:52

Hi! Today I'll tell you about a cool Rust 🦀 trick: **dual-trait pattern!** It's especially useful when you're dealing with the `dyn` keyword and want to simulate OOP features in Rust.

The key idea is that we'll consider **two perspectives** (dual!) about a trait:

* **implementer** of the trait,
* **user** of the trait.

In the end, we will take a look at the dual-trait pattern in the wild - I’ve successfully applied the dual-trait pattern to an [Apache project](https://github.com/apache/datafusion/pull/5521). 😉

## Animal zoo

Let's start with a classic OOP example with animals. Each animal will:

* have a defined species (parrot, monkey, etc.),
* respond to a text command.

Management of the zoo favors diversity, so we must make sure, that each new animal is distinct from the already owned ones (it implies the use of the `Eq` trait).

As a final requirement, our Rust library cannot assume a predefined set of species in the zoo. This (artificial restriction) precludes the use of `enum` and forces out to use `dyn Animal` in the software.

## Let's do it!

First, we'll define the `Animal` trait:

```rust
pub trait Animal: Eq + Debug {
    fn species(&self) -> &'static str;
    fn react(&mut self, command: &str) -> String;
}
```

Notice, that aside from the species and reaction we implement `Eq` and `Debug`.

The zoo has a beautiful garden with palm trees, let's bring some 🦜 parrots:

```rust
#[derive(PartialEq, Eq, Debug)]
pub enum FeatherColor {
    Red,
    Green,
    Blue,
}

#[derive(PartialEq, Eq, Debug)]
pub struct Parrot {
    feather_color: FeatherColor,
}
```

Let's see, how it's natural 🌿 to implement an `Animal` trait. The `Eq` trait was automatically implemented through `#[derive(...)]`.

```rust
impl Animal for Parrot {
    fn species(&self) -> &'static str {
        "Parrot"
    }

    fn react(&mut self, command: &str) -> String {
        match command {
            "repeat" => "Polly want a cracker".to_string(),
            _ => "Squawk!".to_string(),
        }
    }
}
```

The zoo has empty cages. Let's fill them with 🐵 monkeys:

```rust
#[derive(PartialEq, Eq, Debug)]
pub enum FurColor {
    Brown,
    Black,
    White,
}

#[derive(PartialEq, Eq, Debug)]
pub struct Monkey {
    fur_color: FurColor,
}

impl Animal for Monkey {
    fn species(&self) -> &'static str {
        "Monkey"
    }

    fn react(&mut self, command: &str) -> String {
        if command.starts_with("invite") {
            let who = command.split_whitespace().last().unwrap_or_default();
            format!("Oooh oooh aah aah {}", who)
        } else {
            "Aaaah!".to_string()
        }
    }
}
```

## The zoo doesn't compile!

The zoo is just an array of animals:

```rust
pub struct Zoo {
    animals: Vec<Box<dyn Animal>>,
}
```

However, it gives a compiler error 🛑:

```
the trait `Animal` cannot be made into an object
```

The problem is that with the `dyn` keyword we requested a polymorphic version of `Animal` trait, however, it's impossible, because the equality `eq` from `PartialEq` uses `Self` type:

```rust
fn eq(&self, other: &Self) -> bool;
```

But in polymorphic dispatch, we don't know the type of `Self`...

At first glance, it might look like a limitation of Rust. However, in Java and C# we have a similar problem. They solved it by taking `Object` as an argument, not a specific type:

```csharp
public bool Equals(Object obj)
```

So each implementation must cast the `obj` to the `Self` type manually, just like in [C# documentation](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-object-equals):

```csharp
// C# code below, kind of a better Java

// 1. Self is Person6 class
public class Person6
{
    // some person fields...
    private string idNumber;

    // 2. Taking Object, not Person6 as an argument
    public override bool Equals(Object obj)
    {
        // 3. Casting to Self
        Person6 personObj = obj as Person6;

        if (personObj == null) {
            // 4. Not Person6
            return false;
        } else {
            // 5. Got another Person6. Comparing all fields manually.
            return idNumber.Equals(personObj.idNumber);
        }
    }
}
```

## Object safe animals

![A 3D rendering showing parrots and monkeys playing with toys, like cylinders and cubes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8d2lv46vvxlaamlffzvz.jpeg)

We must introduce a new version of the `Animal` trait - an **object safe** one (it means it can be used with the `dyn` keyword)!

```rust
pub trait Animal: Debug {
    fn dyn_eq(&self, other: &dyn Animal) -> bool;
    fn as_any(&self) -> &dyn Any;

    fn species(&self) -> &'static str;
    fn react(&mut self, command: &str) -> String;
}
```

This differs from the original `Animal` trait:

* there is no `Eq` trait, so we have no method with a `Self` type parameter,
* we've introduced `dyn_eq`, taking *any* animal,
* the `as_any` returns us an instance of the [`Any`](https://doc.rust-lang.org/std/any/index.html) trait - which allows us to perform downcasting.

As you'll notice, the equality implementation will be similar to the one in the C# example above. Let's implement the trait for parrot:

```rust
#[derive(PartialEq, Eq, Debug)]
pub struct Parrot {
    // the same...
}

impl Animal for Parrot {
    fn dyn_eq(&self, other: &dyn Animal) -> bool {
        // 1. Downcasting, to check whether the other animal is a parrot
        match other.as_any().downcast_ref::<Self>() {
            // 2. It's a parrot, let's use a comparison from Eq
            Some(o) => self == o,

            // 3. Not a parrot
            None => false,
        }
    }

    fn as_any(&self) -> &dyn Any {
        self
    }

    fn species(&self) -> &'static str {
        // the same...
    }

    fn react(&mut self, command: &str) -> String {
        // the same...
    }
}
```

It'll work, however, it has a major drawback - we've lost the simplicity of just deriving an `Eq` and calling it a day. If we had more traits with `Self` to support, like `Hash` and `Clone`, the `Parrot` implementation would become cluttered.

What is worse, each new `Animal` must implement this routine:

```rust
#[derive(PartialEq, Eq, Debug)]
pub struct Monkey {
    // the same...
}

impl Animal for Monkey {
    // 1. Copy and paste from Parrot
    fn dyn_eq(&self, other: &dyn Animal) -> bool {
        match other.as_any().downcast_ref::<Self>() {
            Some(o) => self == o,
            None => false,
        }
    }

    // 2. Copy and paste from Parrot
    fn as_any(&self) -> &dyn Any {
        self
    }

    fn species(&self) -> &'static str {
        // the same...
    }

    fn react(&mut self, command: &str) -> String {
        // the same...
    }
}
```

## Zoo

We can finally implement our zoo! Diversity requirements are satisfied with `dyn_eq`.

```rust
pub struct Zoo {
    // 1. This time it compiles
    animals: Vec<Box<dyn Animal>>,
}

pub struct InvalidAnimalError {
    pub animal: Box<dyn Animal>,
}

impl Zoo {
    pub fn new() -> Self {
        Zoo {
            animals: Vec::new(),
        }
    }

    pub fn add(&mut self, animal: Box<dyn Animal>) -> Result<(), InvalidAnimalError> {
        // 2. Notice, how dyn_eq is used
        let already_exists = self.animals.iter().any(|a| a.dyn_eq(animal.as_ref()));

        if already_exists {
            // 3. Tigers will have a feast
            Err(InvalidAnimalError { animal })
        } else {
            // 4. Go to a cage for the rest of your life!
            self.animals.push(animal);
            Ok(())
        }
    }
}
```

## The dual-trait pattern

![Parrot with a magic wand, creating new objects, like cylinders and cubes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k8m9xyuorxf3hu27k22t.jpeg)

There we have a collision between an *implementer* of the trait and a *user* of the trait.

* `Animal` developer wants to use cool `#[derive(Eq)]` features and strong static typing.
* The `Zoo` developer wants to have a more OOP-like style approach, supporting `dyn` and requiring tedious downcasting from animals.

The solution is to have... two traits! And we've already written them in this article!

First, let's make the **easy to implement** variant of the `Animal` trait:

```rust
/// This trait facilitates the implementation of the [`Animal`] trait.
pub trait AnimalCore: Eq + Debug + 'static {
    fn species(&self) -> &'static str;
    fn react(&mut self, command: &str) -> String;
}
```

Notice, that it's *not* object safe, because we use `Eq`. Another change is the `Core` suffix in the trait's name. However, it's easy to implement, just like in the "Let's do it!" section.

Let's make the **easy to use** `Animal` trait:

```rust
/// The [`AnimalCore`] trait is *the recommended way to implement* this trait.
pub trait Animal: Debug {
    fn dyn_eq(&self, other: &dyn Animal) -> bool;
    fn as_any(&self) -> &dyn Any;

    fn species(&self) -> &'static str;
    fn react(&mut self, command: &str) -> String;
}
```

It's the same trait as in the "Object safe animals" section. Of course, this *is* object safe.

And now the final trick 🎩🪄 - with Rust's *blanket implementation* the language will automatically implement an object safe variant for each `AnimalCore`

```rust
// 1. For each AnimalCore, we'll implement Animal
impl<T: AnimalCore> Animal for T {
    fn dyn_eq(&self, other: &dyn Animal) -> bool {
        // 2. The OOP downcasting hell
        match other.as_any().downcast_ref::<Self>() {
            Some(o) => self == o,
            None => false,
        }
    }

    fn as_any(&self) -> &dyn Any {
        self
    }

    fn species(&self) -> &'static str {
        // 3. Delegate to the original implementation
        AnimalCore::species(self)
    }

    fn react(&mut self, command: &str) -> String {
        // 4. Delegate to the original implementation
        AnimalCore::react(self, command)
    }
}
```

So... that's it! With that, you just implement the `AnimalCore` trait with idiomatic `#[derive(Eq)]`, and Rust will automatically provide you with an object-safe variant, which can be put in a zoo.

As an exercise, you can rewrite the zoo using [`HashSet`](https://doc.rust-lang.org/std/collections/struct.HashSet.html), so the addition of an animal will take a constant time. This will require you to support hashing with the `dyn_hash` function. You can try the exercise in 🏀 [the Rust playground!](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=0f27ce1ce554f7c13b9b76f97a56be09)

## One more thing...

As a last whiff, we should make it possible to compare the `&dyn Animal` with the `==` operator.

```rust
// 1. Notice the dyn keyword
impl PartialEq for dyn Animal {
    fn eq(&self, other: &Self) -> bool {
        // 2. Delegating to the polymorphic dyn_eq
        self.dyn_eq(other)
    }
}

impl Eq for dyn Animal {}
```

So now you can use the `==` operator in the `add` function:

```rust
let already_exists = self.animals.iter().any(|a| a == &animal);
```

Or even better, use the [`contains`](https://doc.rust-lang.org/std/primitive.slice.html#method.contains) algorithm from the standard library compatible with the `Eq` trait:

```rust
let already_exists = self.animals.contains(&animal);
```

## Drawbacks

The dual-trait pattern makes maintenance of the trait itself more complex. Fortunately, this complexity does not impact implementers or users of the pattern.

The performance shouldn't be impaired, except for the polymorphic calls themselves, rest of the code will be probably inlined.

Using the `dyn` keyword for file interfaces and I/O is usually a good choice. However, if you need to support complex traits like `Eq` or `Hash`, then you should first try to use `enum` and generics.

As a last resort, go for the dual-trait pattern to simulate classic OOP.

## In the wild

![Animals escaping from the zoo using TNT](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ccy5in9v1yajpoq7arxr.jpeg)

Animals somehow escaped into the wild... 🐾

It turns out that the dual-trait pattern is used in a *production Rust* software.

**Apache DataFusion** is an SQL query engine developed in Rust, used by [Apple](https://arrow.apache.org/blog/2024/03/06/comet-donation/) and [InfluxDB](https://www.influxdata.com/glossary/apache-datafusion/).

I've invented 😎 this dual-trait pattern for the purposes of the logical planner, as seen in [this merged PR](https://github.com/apache/datafusion/pull/5521). The problem was that the nodes in the plan (filter, select, etc.) had to support at the same time:

* equality `Eq` and hashing `Hash`,
* custom nodes with `dyn` keyword.

This prompted the use of the dual-trait pattern. Therefore there are two traits:

* `UserDefinedLogicalNodeCore` for an implementer,
* `UserDefinedLogicalNode` object-safe variant for a user.

There is a neat example, of how a third party project belonging to [the Linux Foundation](https://delta.io/), is implementing `UserDefinedLogicalNodeCore`: [`MetricObserver` in delta-rs](https://github.com/delta-io/delta-rs/blob/937015198df144a91e6ff73b1429d2fd1e23a588/crates/core/src/delta_datafusion/logical.rs#L5). The developer had to use only `#[derive(Debug, Hash, Eq, PartialEq)]` to get `dyn_eq` and `dyn_hash` implemented.

## Conclusions

Usually, your Rust types should be modeled with `struct` and `enum` from *functional paradigm*. However, when there is a need for OOP classes, just like in the logical planner example, then the dual-trait pattern should resolve this 🎯 *object-functional impedance*.

If you're into 🦀 Rust, then you might enjoy my other *dev.to* article [Array duality in Go and Rust](https://dev.to/mslapek/array-duality-in-go-and-rust-4pep), comparing various ways to allocate an array in Rust, like `Vec` or `Cow`.

Comments 💬 and questions are welcome! Don't forget to check out 🏀 [the Rust playground!](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=0f27ce1ce554f7c13b9b76f97a56be09)