Walking through "The Java Tutorials" with Rust

Boxed trait objects and the search for inheritance

A sunny friday in June 2021

Boxed Trait Objects

The dyn keyword from the previous page allowed us to create a local variable with type dynamically resolved at compile time. We had to allocate the space for our objects (basic and mountain bikes) in advance and conditionally point the variable to one of them at runtime.

Going a step further, we can use Rust's standard library Box<T> to put our random, runtime chosen bike on the heap so we won't have to worry about it's size and allocation as in stack variables.

#[test]
fn random_bike() {
    let coin : bool = rand::random();
    // This feels less verbose then the previous, stack based example

    // bike is a pointer to something on the heap that implements the Bicycle Trait
    let mut bike: Box<dyn Bicycle> = if coin {
        Box::new(BasicBicycle { cadence: 0, speed: 0, gear: 0})
    } else {
        Box::new(MountainBicycle { cadence: 0, speed: 0, gear: 0, tires: "mountain tires".to_string()})
    };

    // Box implements the `Deref` and `DerefMut` traits so we don't have to dereference it ourselves
    bike.speed_up(123);
    bike.print_states();
}

Pointers and objects as balloons 🎈

Reading about pointers and dereferencing them would usually make me run away, and Java always felt as a sweet spot with explicit types but without messing with pointers directly (imagine pointers to objects flying in a binary crowded space). I find Rust's approach regarding these points appealing:

  • you will hold pointers. there is an explicit way to ask for a reference/dereference of something with the & and * symbols - but there are language mechanisms to simplify it like Implicit Deref Coercion with function and methods.
  • Objects won't fly wherever they want to: the memory will be freed when the reference/pointer is dropped. And the borrow checker will prevent more then one mutable reference (*unless you ask for it)

Inheritance (postponed?)

Boxed trait objects enables the runtime polymorphism that we wanted in the small bike example above, but does not directly provide a way to share state declarations between Objects. We still can't define that MountainBicycle is inheriting speed and other members from BasicBicycle, like Java's

class MountainBike extends BasicBicycle

One could argue that's sharing state isn't a good idea, and that you'd be better with sticking to sharing behavior with traits. Even with java I to prefer having a class that implements some Interface instead of extends that inherits members and methods from a base class:

  • members are an implementation detail
  • it feels like a good and precise way to describe what this class is capable of.
  • java doesn't support multiple inheritance, but allow implementing many interfaces.

But still, in the discussed bikes example from The Java Tutorials there is hierarchy between objects, so let's modal the same relationship with rust.

Apparently this is a known "issue" with several ideas, RFCs, and discussions. I'm a bit overwhelmed by all the directions it could take, and look forward to see updates on this. See for example meeting minutes from 2014 here. I'll just mention 2 suggestions that pop to my mind and are relevant for our example:

Common fields, refinement types and nested enums

described in detail (among other related ideas) here. In the context of the bikes example, it would allow to define common-fields that every bike have (and deal with both Bicycle enum and it's MountainBicycle variant as types).

Delegation RFC

A proposal to add a delegate keyword as a syntactic sugar for delegation of behavior to a member.

impl TR for S {
    delegate * to self.f;
}

This RFC is postponed right now, but as it's interesting to read about it. links: rust-lang RFC: Delegation, and Discussion here.

Modeling hierarchy in Rust

Composition

Ok, I understand that rust hasn't decided on if and how to enable java like inheritance, but let's try to model the relation between our BasicBicycle and MountainBike through composition (technically, this is similar to the delegation RFC above but without the added sugar).

So, suppose that MountainBicycle is a BasicBicycle with special tires, we can write something like this:

struct BasicBicycle {
    cadence : i32,
    speed: i32,
    gear: i32
}

// MountainBicycle has an inner BasicBicycle
struct MountainBicycle {
    basic_bicycle: BasicBicycle,
    tires: String
}
trait Bicycle {
   // same as before
   ...
}

// MountainBicycle delegates it's Bicycle implementation to it's inner basic_bicycle
impl Bicycle for MountainBicycle {
    fn get_cadence(&self) -> i32 {
        self.basic_bicycle.cadence // or get_cadence()
    }

    fn get_speed(&self) -> i32 {
        self.basic_bicycle.speed // or get_speed()
    }

    fn get_gear(&self) -> i32 {
        self.basic_bicycle.gear
    }

    fn get_model(&self) -> String {
        "mountain".to_string()
    }

    fn speed_up(&mut self, increment : i32) {
        self.basic_bicycle.speed += increment;
    }

    fn print_states(&self){
        println!("model: {} cadence: {} speed: {} gear: {} tires: {}", &self.get_model(), &self.get_cadence(), &self.get_speed(), &self.get_gear(), &self.get_tires());
    }
}

// rust doesn't have constructors, but implementing the Default trait for MountainBicycle would allow easier creation
// of MountainBicycle without manually creating the inner BasicBicycle at caller site.
impl Default for BasicBicycle {
    fn default() -> Self {
        BasicBicycle { cadence: 0, speed: 0, gear: 0}
    }
}

impl Default for MountainBicycle {
    fn default() -> Self {
        MountainBicycle { basic_bicycle: BasicBicycle::default(), tires: "mountain tires".to_string()}
    }
}

The small test we had still runs:

fn random_bike() {
    let coin : bool = rand::random();

    let mut bike: Box<dyn Bicycle> = if coin {
        Box::new(BasicBicycle::default())
    } else {
        Box::new(MountainBicycle::default())
    };
    bike.speed_up(123);
    bike.print_states();
}

Enum variants

I guess that if I had a something like this to code I'd just go with enum variants as shown in a previous post. The main limitation that I see here when one needs to enable others to extend the list of Bicycle variants, outside of crate boundaries (AFAIK the straightforward way to go if you are a library author that want your users to add new bikes, is to define a trait and let them implement it for their types, maybe providing assisting data types and some default implementations - this is fully supported in rust).