10 Mar 24

Rust structs compared to Ruby classes
Originally posted on Medium

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 endendmy_user = User.newmy_user.kind = User.adminmy_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.kindputs my_user.kind::NAMEputs 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; endclass NonAdmin; endclass User USER_KINDS = [Admin, NonAdmin] attr_accessor :kind # class method def self.admin USER_KINDS.first end # instance method def get_kind @kind endendmy_user = User.newmy_user.kind = User.adminputs 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.