published on in development Rust

Structs, traits and OOP in Rust

Rust is quite an opinionated language when it comes to its idioms. You can do Object Oriented Programming (OOP) as a programming style just fine with it but things are in slightly different places than you’re used to when coming from places like C++ or Java. There’s a whole chapter on this subject in Rust’s official documentation, so I won’t rehash that here. The main point being: Rust may or may not be Object Oriented according to which scientist you ask. I just want to write secure, performant and maintainable software. Oh and I still don’t know what I’m doing, these are just my own personal notes!

Structs

Structs in Rust are somewhat similar, on the surface, to how they look in languages like C. Rust, however, has a bunch of tricks up its sleeve to make structs quite a bit more powerful than they are in older languages. It’s good to remember, though, that a struct in Rust is not the same thing as a class in many other languages. For one thing, there’s no such thing as inheritance in Rust. Now before you break down in a hissy fit over this: hold your horses until you learn about traits and enums as well.

So pulling an example of a struct from the official Rust docs:

struct User {
  username: String,
  email: String,
}

That’s fairly straight forward. The struct User groups together a number of related variables. This allows us to use natural language that’s more closely aligned with the business purpose of our software. Speaking of User structs is much clearer than trying to juggle the individual variables and their types.

Or to quote the official docs again:

A struct, or structure, is a custom data type that lets you package together and name multiple related values that make up a meaningful group. If you’re familiar with an object-oriented language, a struct is like an object’s data attributes.

Traits

In my limited experience traits are a feature unique to Rust. They are similar in nature to interfaces in Java or abstract classes in C++. Like I said above Rust doesn’t support inheritance in the classic OOP sense of the word. So let’s see how they do work. The Rust standard library defines a trait named Display that’s used by the println! macro to display things on our console. So if logic holds, we could implement the std::fmt::Display trait for our User struct and it’d allow us to just println! any instance of it as we see fit, without knowing anything about the struct’s internals or the trait’s implementation.

impl std::fmt::Display for User {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(f, "{} is a User who can be e-mailed at {}.", self.username, self.email)
  }
}

fn main() {

Now all that’s left is for us to define our main() function so the program has a place to start from, define an instance of our struct and print it out to the console. Something like this:

fn main() {
  let user = User {
    username: String::from("Area536"),
    email: String::from("info@area536.com"),
  };
  println!("{}", user)
}

Once you run this, it should output:

Area536 is a User who can be e-mailed at info@area536.com.

So in short, this means that traits specify the way we want our structs and their methods to be used from the outside. Sure there’s still a lot of unexplained stuff in our implementation of Display, but conceptually I learned a few things here. Relating to the original program I’m trying to write, my utility to wrangle virtual disks for my retro computing hobby, here’s what I have now:

use clap::Parser;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
    /// Name of the person to greet
    #[clap(short, long)]
    path: String,

    /// Number of times to greet
    #[clap(short, long, default_value_t = 1)]
    count: u8,
}

fn main() {
  let args = Args::parse();
  println!("Value: {}", args.path);
}

This was the initial result of trying to get my command line tool to accept parameters. That first line with the ‘derive’ incantation felt like unexplained magic to me:

#[derive(Parser, Debug)]

As it turns out, it defines derived traits for the struct below. In this case two of them: Parser and Debug. As it turns out, the lines beginning with a # are expanded into a whole lot of Rust code by the compiler at build time. In our case here the compiler fully automatically generates complete trait-implementations without us having to worry about any of them.

Of course the code for actually doing this is defined elsewhere but that’s for another time. I’m starting to figure out how these constructs are all tied together.

The main takeaway is that these are macros, and macros generate Rust code as an intermediate step before the compiler actually constructs a tailor-made binary for us. There’s a whole lot of depth to thir rabbit hole and most of it is actually covered in the official docs. Read up on prodecural macros to find out how you can define your own derived traits. The subject is far too advanced for me to dive into. For now, I’m going to focus on how to create and write to a file without breaking things.