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.