09 Feb 24

Rust Structs compared to Ruby classes

Wait, the title says we are comparing Structs and classes, but Ruby has a Struct object, what gives? It does, we’ll get into it.

If you’ve been following along, we haven’t gotten into code structure yet. We’ve scratched the surface of classes and objects in the last post about Enums but in this post, we’re going to lay out the foundation of “classes” in Rust compared to Ruby.

In the Enum post, we created a User class in Ruby with some constants that offered us a way to illustrate an equivalent to Rust Enums. The class and output looked like this:

class User
    USER_KINDS = [Admin, NonAdmin]
    USER_SCORES = [HighScore, LowScore]
    USER_CONTACTS = [Electronic, Physical]

    attr_accessor :kind, :high_score, :low_score, :contact

    def self.admin
      USER_KINDS.first
    end

    def self.nonadmin
      USER_KINDS.last
    end

    def self.high_score(value)
      USER_SCORES.first.new(value)
    end

    def self.low_score(value)
      USER_SCORES.last.new(value)
    end

    def self.contact(email:, text:, city:, state:)
      if !email.nil? || !text.nil?
        USER_CONTACTS.first.new(email: email, text: text)
      elsif !city.nil? || !state.nil? 
        USER_CONTACTS.last.new(city: city, state: state)
      end
    end
end

my_user = User.new
my_user.kind = User.admin
my_user.low_score = User.low_score(30)
my_user.contact = User.contact(email: "user@example.com", text: "1235131234", city: nil, state: nil)

puts my_user.kind
puts my_user.kind::NAME
puts my_user.low_score.score

Let’s trim this down and then work on an equivalent in Rust that illustrates how “classes” and class methods are defined. We’ll go with this.

class Admin; end
class NonAdmin; end

class User
    USER_KINDS = [Admin, NonAdmin]

    attr_accessor :kind

    # class method
    def self.admin
      USER_KINDS.first
    end

    # instance method
    def get_kind
        @kind
    end
end

my_user = User.new
my_user.kind = User.admin

puts my_user.get_kind # => prints Admin

Now let’s look at a version of the same thing in Rust. A couple of notes, in Rust we’re going to use a class method to return the array that was defined as a constant in Ruby. It keeps things simple. Namespaces would come into play if we defined it as a constant. We’ll get into namespaces in a future post. Let’s stick to what a “class” looks like in Rust.

pub struct User {
    kind: &'static str,
}

impl User {
    // note rather than a constant (with a scope) we are using a function
    fn user_kinds() -> Vec<&'static str> {
        vec!["Admin", "NonAdmin"]
    }

    fn admin() -> &'static str {
        Self::user_kinds()[0]
    }

    fn get_kind(&self) -> &'static str {
        self.kind
    }
}

let user = User {
    kind: User::admin(),
};
println!("user kind: {}", user.get_kind());

There is quite a bit to walk through here. Let’s start with the most obvious question. Is there no “class” keyword in Rust? Nope. We define “classes” as Structs in Rust. Is a Rust Struct the same as a Ruby Struct? Nope. Is a Rust Struct the same as a Ruby class? Um, kinda. Let’s look at the details.

A Rust structure is just the definition of the object. You can create instances of it. For example, in the above code struct User is the equivalent of Ruby’s class User. But instantiating looks different. In Ruby, we are used to seeing User.new(). In Rust, we use the struct syntax and define the attribute values. For example:

User {
    kind: User::admin()
}

Because we defined the struct and said there was a kind attribute with a string as a value, our instantiation has to provide a string value for a kind attribute. If User::Admin() didn’t return a string, we’d receive a compile error.

Now notice the impl User block. This is where we define the class and instance functions for the User struct. We’ve defined both a class and instance function as an example. Class functions are straightforward. Whereas in Ruby, we are used to seeing def self.whatever() as the method declaration of a class method. self means “this class”. In Rust, we omit the self keyword and just define the function. It becomes a class function.

Instance functions, however, must define &self as the first attribute. Think of it as a callee always passing in the instance. Our get_kind function is an example.

You may want to note that calling a class function looks different in Rust vs Ruby. In Ruby, we’d call self.admin(), but in Rust, we call Self::admin(). Just a difference in syntax.

This small example illustrates quite a lot. There is no class in Rust. We use Struct instead. We open a class and define its attributes and methods in Ruby. In Rust, we define the struct with the attributes and then implement functions for it. Calling methods have similar syntax but how they are defined differs. As we mentioned, instance functions specify &self as the first argument. That’s a lot to digest.

In the next post, we’ll go further into structure and talk about modules, autoloading, and paths, and compare them to things like Zeitwerk in Rails.