Basic Object Oriented Programming in Rust

This is great! I will try to test it later this week.

If you're coming from Java, the question is usually less about “how to do OOP in Rust” and more about how to organize code without classes and familiar DI frameworks.

Rust pushes you toward composition + traits instead of classical OOP, so the structure ends up quite different.

I went through the same transition and explored how to build something similar to Java-style DI, but without runtime cost by pushing everything to compile time.

This might be closer to what you're looking for.

Here’s a concrete example with explanation and code:

https://medium.com/@amid.ukr/can-rust-have-zero-cost-dependency-injection-fc0c9ae6abd3

As a side note, if you're aiming for maximum performance, it's worth being careful with dyn and other runtime abstractions. In many cases, Rust lets you move that cost to compile time. The article also goes into this.

I believe I should try-out @mroth's solution before I take any further step. With respect to OOP, my existing implementations support a base-class that offer a (virtual) encode() method. This class is then extended by object-specific (int, string, array, etc.) classes having object-specific methods and constructors. I believe this can be emulated using traits but I'm not there yet and may never [have to] be either.

The JavaScript and Python implementations are anything but fast. I refer to them as "Reference Implementations" :wink: They should be rewritten in C or Rust but that is a task for the platform maintainers the day they find CBOR::Core support an asset.

Speaking about @mroth I guess you mean cbor-core?

CBOR - Wikipedia - what I am seeing it another binary serialization format, looks like you trying to understand how OOP works in rust.

Rust is not classic OOP language like Java and C++. You can do certain OOP things, but Rust never targeted to be complete OOP language, it uses slightly different model.

Let me check your example more clearly.

  1. You have struct: Dog and Cat.
  2. You have trait Speak
  3. You've implemented trait Speak for Dog and Cat.

Ok. I see you have an enum, and you trying to call dog call on that enum, you need a pattern matching here.

I am using explicit type Animal from you main method to make it clear;

   let dog:Animal = Animal::Dog(Dog{});

Actually you dog is not of type Dog, it is of type Animal
And you can't use enum option as a seperate type
So you can't do something like: let dog: Animal::Dog = Animal::Dog(Dog{});

To extract dog from enum, you need patter matching:

    match dog {
        Animal::Dog(dog) => Dog::only_for_dogs(),
        _ => panic!("Other options not expected")
    };

Also I am nut sure, if it was your real intention to use static method here, since you playing with OOP, the Animcal::dog is declared as static function, let's redo into instance method:

impl Dog {
    pub fn only_for_dogs_self(&self) {    }
}

so the pattern matcher will look like this:

    match dog {
        Animal::Dog(dog) => dog.only_for_dogs_self(),
        _ => panic!("Other options not expected")
    };

Hope it will answer your question.


Regarding this:

impl Speak for Animal {
    fn speak(&self) -> String {
        // >>>> This is obviously entirely wrong, it is recursion!
        // self.speak() <-- this will not work, because self is enum, not any of yours struct
        // you need pattern matcher again here.

        match self {
             Animal::Dog(_) => Dog::speak(),
             Animal::Cat(_) => Cat::speak();
        }
    }
}

Same pattern matching.

It also quite classic scenario for delegation. Rust idiology is like composition over inheritance, and maybe some proc-macro specially designed for delegation can reduce boilerplate code.

Thanx! I hope this thread can be of some use for other people having another background and would like to (in some way) emulate OOP.

One comment: if Dog has a whole bunch of specific methods, the match approach seems a bit awkward. I would consider a get_dog() method giving a handle to the dog object and then perform method calls on that object. In CBOR that would apply to the map and array types.

I released cbor-core@0.5.1 / CHANGELOG:

The most notable updates are optional support for crates such as serde, chrono, time, and several big-integer crates. Another important improvement is the addition of mitigations against malicious inputs.

Functionally, I think it is getting close to feature-complete. So I’d be very interested in reports of anything that does not work, feels missing, or looks weird.

Thx!

Yes, I agree. If Dog has many type-specific methods, writing explicit match delegation for every method becomes cumbersome.

I tried another approach, which was already hinted at in one of the comments:

impl Animal {
    fn get_speak(&self) -> &dyn Speak {
        match self {
            Self::Dog(x) => x,
            Self::Cat(x) => x,
        }
    }
}

Here I use pattern matching once to convert Animal into &dyn Speak, and then use that interface:

fn main() {
    let dog = Animal::Dog(Dog {});
    println!("Animal says: {}", dog.speak());

    let speak: &dyn Speak = dog.get_speak();
    println!("Speak = {}", speak.speak());
}

That also lets me rewrite Speak for Animal more cleanly:

impl Speak for Animal {
    fn speak(&self) -> String {
        self.get_speak().speak()
    }
}

At first I was concerned about dyn Speak, since trait objects usually imply some runtime overhead through dynamic dispatch and a vtable.

However, when I generated assembly for release mode with:

cargo rustc --release -- --emit asm

the optimizer handled this case quite well. In such a simple example, it was able to remove the obvious overhead, so the generated code was better than I initially expected.

In general, I still think it is worth being careful with dyn if performance is critical. If you are choosing Rust, you likely care about performance already. But in examples like this one, the optimizer can sometimes eliminate much of the cost.


One more thing that may be useful to mention, since this discussion touches Rust enums.

Rust enums are quite different from Java enums. In Java, an enum is mostly a fixed set of named constants, all of the same type. In Rust, an enum is a full data type whose variants can carry different kinds of data.

For example:

enum MyEnum<T> {
    StringValue(String),
    IntValue(i32),
    IntFloatValue(i32, f32),
    CustomValue(T),
}

And then pattern matching becomes very natural:

use std::fmt::Debug;

fn print_enum<T: Debug>(enum_value: &MyEnum<T>) {
    match enum_value {
        MyEnum::StringValue(x) => println!("StringValue = {}", x),
        MyEnum::IntValue(x) => println!("IntValue = {}", x),
        MyEnum::IntFloatValue(x, y) => println!("IntValue = {}, FloatValue = {}", x, y),
        MyEnum::CustomValue(x) => println!("CustomValue = {:?}", x),
    }
}

Usage:

print_enum(&MyEnum::<Vec<i32>>::StringValue("string".to_owned()));
print_enum(&MyEnum::<Vec<i32>>::IntValue(6));
print_enum(&MyEnum::<Vec<i32>>::IntFloatValue(5, 1.0));
print_enum(&MyEnum::<Vec<i32>>::CustomValue(vec![1, 2, 3]));

So in Rust, an enum is not just a symbolic constant. It is more like a tagged union or algebraic data type. In this example, it is not just used as a constant, but also carries data that is later used in pattern matching.


The key idea is not “we chose enum, so we must use pattern matching,” but rather the opposite: if pattern matching is a natural fit for the problem, then enum is usually the right tool.

In your original example, if pattern matching starts to feel awkward, that is often a signal that enum might not be the best abstraction there. In such cases, it may be worth looking at other approaches, such as trait-based design or generics, depending on what you are trying to model.

This will be my final post about CBOR::Core in this thread. A new release is available:

Cbor-core 0.6.0: A deterministic CBOR::Core encoder/decoder

This will be my final post about CBOR::Core in this thread.

Michael, you have done an amazing job and in a very short time. The code looks very neat as well. I will start trying out features while (slowly...) becoming a bit more "Rusty".

I’ve found one interesting macro-crate, for a task you’ve described, it can reduce a lot of boilerplate code:

use enum_dispatch::enum_dispatch;

#[enum_dispatch]
trait MyTrait {
    fn do_some(&self);
}

struct A1;
struct A2;

impl MyTrait for A1 {
    fn do_some(&self) {
        println!("A1");
    }
}

impl MyTrait for A2 {
    fn do_some(&self) {
        println!("A2");
    }
}

#[enum_dispatch(MyTrait)]
enum My {
    A1,
    A2,
}

Thanks! It sure looks pretty. What I don't get is how this code could handle data (fields) associated with A1 and A2 that may be entirely different. For a solution to be comparable to what I have done in Java, JavaScript, and Python objects also need to be accessible in a shared mode. As a proof-of-concept I came up with this:

// Downscaled CBOR encoder trying out another approach

use std::{cell::RefCell, rc::Rc};

trait CoreTrait {
    fn to_string(&self) -> String;
}

#[derive(Clone)]
#[derive(Debug)]
struct ArrayContent {
    vector: Rc<RefCell<Vec<CBOR>>>
}

#[derive(Clone)]
#[derive(Debug)]
struct IntContent {
    value: i64
}

impl CoreTrait for IntContent {
    fn to_string(&self) -> String {
        self.value.to_string()
    }
}

impl CoreTrait for ArrayContent {
    fn to_string(&self) -> String {
        let mut string = String::new();
        string.push('[');
        let array = (*self.vector.borrow()).clone();
        let mut n = 0;
        while n < array.len() {
            if n > 0 {
                string.push(',');
            }
            string.push_str(&array[n].to_string());
            n += 1;
        }
        string.push(']');
        string
    }
}

impl ArrayContent {}

#[derive(Clone)]
#[derive(Debug)]
enum CBOR {
    Array(ArrayContent),
    Int(IntContent)
}

impl CBOR {
    pub fn new_array() -> CBOR {
        CBOR::Array(ArrayContent {vector: Rc::new(RefCell::new(Vec::new()))})
    }

    pub fn new_i64(value: i64) -> CBOR {
        CBOR::Int(IntContent {value: value})
    }

    pub fn get_i64(&self) -> i64 {
        match self {
            CBOR::Int(int_content) => int_content.value,
            _ => panic!("Not an integer: CBOR::{:?}", self)
        }
    }

    pub fn get(&self, index: usize) -> CBOR {
        match self {
            CBOR::Array(map_content) => {
                (*map_content.vector.borrow())[index].clone()
            },
            _ => panic!("Not an array : CBOR::{:?}", self)
        }
    }

    pub fn add(&self, cbor_object: CBOR) -> CBOR {
        match self {
            CBOR::Array(map_content) => {
                map_content.vector.borrow_mut().push(cbor_object);
                self.clone()
            },
            _ => panic!("Not an array : CBOR::{:?}", self)
        }
    }

    pub fn add_ref(&self, cbor_object: &CBOR) -> CBOR {
        match self {
            CBOR::Array(map_content) => {
                map_content.vector.borrow_mut().push(cbor_object.clone());
                self.clone()
            },
            _ => panic!("Not an array : CBOR::{:?}", self)
        }
    }

    pub fn to_string(&self) -> String {
        self.as_trait().to_string()
    }

    fn as_trait(&self) -> &dyn CoreTrait {
        match self {
            CBOR::Array(core_trait) => core_trait,
            CBOR::Int(core_trait) => core_trait
        }
    }
}

fn update_array(array: &CBOR) {
    array.add(CBOR::new_i64(9))
         .add(CBOR::new_array().add(CBOR::new_i64(-177)));
}

fn main() {
    let root_array = CBOR::new_array();
    update_array(&root_array);
    let an_integer = CBOR::new_i64(6);
    let another_array: CBOR = CBOR::new_array();
    another_array.add(CBOR::new_i64(567));
    root_array.add_ref(&another_array);
    another_array.add(CBOR::new_i64(888));
    println!("integer = {}", an_integer.get_i64());
    root_array.add(an_integer).add(CBOR::new_i64(7));
    root_array.get(2).add(CBOR::new_i64(44));
    println!("root array: {}", root_array.to_string());
    println!("another array: {}", another_array.to_string());
    println!("integer = {}", root_array.get(2).get(1).get_i64());
    root_array.get_i64();  // Panic!
}

The code supports "polymorphism" , "encapsulation", and "sharing". However it is still incomplete because it lacks "inheritance" (a common set of data and methods). It would be cool if you could do that without repeating the implementation for every type.

Note: That the methods lack a Result type is because this PoC also builds on a panic-based API. This is though another (and to the subject unrelated) issue.

Hi @cyberphone,

Here is a complete example of how static polymorphism with field access can work in Rust, using macros, with no more code than your dyn Trait example.

As you can see, there is almost no enum boilerplate here. I implement the trait for normal structs as usual, and then use enum_dispatch to automatically implement the same trait for the enum by delegating calls to the inner value.

Below, in the main function, you can see 3 approaches to compile-time polymorphism.

The interesting part is that the same Rust functions that work for structs also work for the enum.

Documentation for used macro: enum_dispatch - Rust

enum_dispatch provides a set of macros that can be used to easily refactor dynamically
dispatched trait accesses to improve their performance by up to 10x.

use enum_dispatch::enum_dispatch;

#[enum_dispatch]
trait SampleToString {
    fn to_string(&self) -> String;
    fn get_type(&self) -> &'static str;
}

struct IntWrapper {
    int_value: i32,
}

struct BoolWrapper {
    bool_value: bool,
}

struct MultiWrapper {
    int_value: i32,
    bool_value: bool,
    string_value: &'static str,
}

impl SampleToString for IntWrapper {
    fn to_string(&self) -> String {
        self.int_value.to_string()
    }

    fn get_type(&self) -> &'static str {
        "IntWrapper"
    }
}

impl SampleToString for BoolWrapper {
    fn to_string(&self) -> String {
        self.bool_value.to_string()
    }

    fn get_type(&self) -> &'static str {
        "BoolWrapper"
    }
}

impl SampleToString for MultiWrapper {
    fn to_string(&self) -> String {
        format!(
            "int = {}, bool = {}, str = {}",
            self.int_value, self.bool_value, self.string_value
        )
    }

    fn get_type(&self) -> &'static str {
        "MultiWrapper"
    }
}

#[enum_dispatch(SampleToString)]
enum WrapperEnum {
    IntWrapper,
    BoolWrapper,
    MultiWrapper,
}

fn handle_as_enum(enum_wrapper: &WrapperEnum) {
    println!(
        "Call to WrapperEnum::{}::to_string: {}",
        enum_wrapper.get_type(),
        enum_wrapper.to_string()
    )
}

fn handle_enum_as_trait_generic<T: SampleToString>(trait_wrapper: &T) {
    println!(
        "Call to {}::SampleToString::to_string: {}",
        trait_wrapper.get_type(),
        trait_wrapper.to_string()
    )
}

fn handle_enum_as_trait_as_impl(trait_wrapper: &impl SampleToString) {
    println!(
        "Call to {}::SampleToString::to_string: {}",
        trait_wrapper.get_type(),
        trait_wrapper.to_string()
    )
}

fn main() {
    let int_wrapper: IntWrapper = IntWrapper { int_value: 42 };
    let bool_wrapper: BoolWrapper = BoolWrapper { bool_value: true };
    let multi_wrapper: MultiWrapper = MultiWrapper {
        int_value: 100,
        bool_value: false,
        string_value: "Some string",
    };

    println!("=========================");
    println!("Test struct polymorphism, no enum yet:");
    println!("  As generic argument");
    handle_enum_as_trait_generic(&int_wrapper);
    handle_enum_as_trait_generic(&bool_wrapper);
    handle_enum_as_trait_generic(&multi_wrapper);
    println!("  As impl argument");
    handle_enum_as_trait_as_impl(&int_wrapper);
    handle_enum_as_trait_as_impl(&bool_wrapper);
    handle_enum_as_trait_as_impl(&multi_wrapper);
    println!();

    let enum_int_wrapper: WrapperEnum = WrapperEnum::IntWrapper(int_wrapper);
    let enum_bool_wrapper: WrapperEnum = WrapperEnum::BoolWrapper(bool_wrapper);
    let enum_multi_wrapper: WrapperEnum = WrapperEnum::MultiWrapper(multi_wrapper);

    println!("=========================");
    println!("Test enum polymorphism");
    println!("  As enum");
    handle_as_enum(&enum_int_wrapper);
    handle_as_enum(&enum_bool_wrapper);
    handle_as_enum(&enum_multi_wrapper);
    println!("  As generic argument");
    handle_enum_as_trait_generic(&enum_int_wrapper);
    handle_enum_as_trait_generic(&enum_bool_wrapper);
    handle_enum_as_trait_generic(&enum_multi_wrapper);
    println!("  As impl argument");
    handle_enum_as_trait_as_impl(&enum_int_wrapper);
    handle_enum_as_trait_as_impl(&enum_bool_wrapper);
    handle_enum_as_trait_as_impl(&enum_multi_wrapper);
    println!()
}

And here is an output:

$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/rust_tests`
=========================
Test struct polymorphism, no enum yet:
  As generic argument
Call to IntWrapper::SampleToString::to_string: 42
Call to BoolWrapper::SampleToString::to_string: true
Call to MultiWrapper::SampleToString::to_string: int = 100, bool = false, str = Some string
  As impl argument
Call to IntWrapper::SampleToString::to_string: 42
Call to BoolWrapper::SampleToString::to_string: true
Call to MultiWrapper::SampleToString::to_string: int = 100, bool = false, str = Some string

=========================
Test enum polymorphism
  As enum
Call to WrapperEnum::IntWrapper::to_string: 42
Call to WrapperEnum::BoolWrapper::to_string: true
Call to WrapperEnum::MultiWrapper::to_string: int = 100, bool = false, str = Some string
  As generic argument
Call to IntWrapper::SampleToString::to_string: 42
Call to BoolWrapper::SampleToString::to_string: true
Call to MultiWrapper::SampleToString::to_string: int = 100, bool = false, str = Some string
  As impl argument
Call to IntWrapper::SampleToString::to_string: 42
Call to BoolWrapper::SampleToString::to_string: true
Call to MultiWrapper::SampleToString::to_string: int = 100, bool = false, str = Some string

Cool. The authors also claim a ten-fold performance improvement! Being a Rust n00b I've focused on how to map traditional OO in Rust. I (involuntary) pissed-off some people by claiming that exception-based APIs sometimes can be quite useful :wink: Not in Linux kernels or UIs, but in server backends and CLIs: An "heretic's" view of Rust error-handling

Anyway, the (for me...) most problematic aspect of Rust turned out to be my wish to "chain" API calls during encoding. This forced me to use the Rc<RefCell construct and more or less outlaw the use of mut. I have verified that the reference counter indeed survives a .clone() operation. Maybe not the most performant code but API convenience often comes at a price.

I also encountered issues with inheritance: Emulating OO inheritance. They are resolved.

To handle error cases, this quite useful: anyhow - Rust

I'm curious why that is? I don't think I've ever seen a builder pattern that needed interior mutability.

In my existing implementations you can for example write (here in pseudo code):

array = createArray().addInt(4).addString("Hi")

That is, the add* methods return self, which in Rust (as far I know...) implies cloning.

Why do these signatures not work for you?

// This is just for example's sake, I assume
// you don't represent you data this way

struct Array(Vec<String>);

impl Array {
  pub fn new() -> Self { Self(Vec::new()) }

  pub fn add_int(mut self, i: i64) -> Self {
    self.0.push(i.to_string());
    self
  }

  pub fn add_string(mut self, s: impl Into<String>) -> Self {
    self.0.push(s.into());
    self
  }
}

fn main() {
  let mut array = Array::new().add_int(4).add_string("Hi");
  // perhaps later...
  array = array.add_int(5);
}
// This is still just for example's sake

struct Array(Vec<String>);

impl Array {
  pub fn new() -> Self { Self(Vec::new()) }

  pub fn add_int(&mut self, i: i64) -> &mut Self {
    self.0.push(i.to_string());
    self
  }

  pub fn add_string(&mut self, s: impl Into<String>) -> &mut Self {
    self.0.push(s.into());
    self
  }
}

fn main() {
  let mut array = Array::new();
  array.add_int(4).add_string("Hi");
  // perhaps later...
  array.add_int(5);
}

(edit: changed the second example to actually compile, whoops)

No cloning is present in either of those examples. Both are considered idiomatic Rust patterns. If you could explain what you mean by it "implies cloning", that might help.

Nope. It's called a "move" and is just a bit copy (a shallow copy) of the variable as with an assignment.

Thanx, it is the mut keyword that made the difference!

Update: in my implementation I ran into Rust ownership issues since the things stored in Vec are wrapped self. I guess this is the most confusing part of Rust...

array1 = createArray()
array2 = createArray().addInt(4).addRef(&array1),addString("Hi")
array1.addInt(6)

In flux version: cbor-rust-examples/check-for-unread/src/main.rs at main · cyberphone/cbor-rust-examples · GitHub