10 Mar 24

Rust enums compared to Ruby objects
Originally posted on Medium

So far we’ve discussed Strings and Numbers which are relatively comparable in Rust and Ruby. At least both languages have those concepts as first-class citizens. In this post, we are going to discuss Enums.

If you are coming from Ruby, you may not be completely familiar. More than likely you’ve heard of Enums in the context of database validation. Typically, we’d use Enums while deciding if a given value is an acceptable value for a database column. In Ruby, a lot of times this is done using a list of acceptable values.

In our project files, we’ll create types of users. The types will be used to illustrate the use of Enums.

In Ruby, we could do something like this:

class Admin   NAME = "Administrator"endclass NonAdmin   NAME = "Application User"endclass User   USER_KINDS = [Admin, NonAdmin]   attr_accessor :kind   def self.admin      USER_KINDS.first   end   def self.nonadmin      USER_KINDS.last   endendmy_user = User.newmy_user.kind = User.admin # we could have used USER_KINDS.firstputs my_user.kind # prints out Adminputs my_user.kind::NAME # prints out Administrator

We could add validation to this to make sure that User#kind is only ever set to a value included in USER\_KINDS, but let’s see what the equivalent is in Rust.

pub struct User {   kind: UserKind,}#[derive(Debug)]pub enum UserKind {   Admin,   NonAdmin,}#[derive(Debug)]pub struct Admin {}impl Admin {   const NAME: &str = "Administrator";}#[derive(Debug)]pub struct NonAdmin {}impl NonAdmin {   const NAME: &str = "Application User";}let my_user = User {   kind: UserKind::Admin,};println!("{:?}", my_user.kind); // prints Admin    let kind = my_user.kind;if let UserKind::Admin = kind {   println!("name: {}", Admin::NAME);}

You can see we declare an object with type enum on line 6. Enum is a first-class citizen in Rust. It is made up of variants. Our variants are Admin and NonAdmin. Each is a struct with a constant implemented defining a unique name.

Notice when we define our `my_user` variable we set a value with a kind. That kind points to the specific variant of the enum. In the example above, that is `UserKind::Admin`. One of the first things users will do is try to access the specific variant through `my_user`. Notice where we print out the value of `my_user.kind`. It says Admin. But if we try `my_user.kind::NAME` to try and access the declared constant in the Admin struct, we’ll receive a compilation error. Our value is a pointer to that variant struct, but not an instance of the struct itself. The last 4 lines show a way of accessing the NAME constant value of the Admin class, in relation to our `my_user` variable. When you are used to the expressiveness in Ruby, this illustration may feel like a slight backpedal just to access a property of an object.

But, we’re not illustrating the utility of Enums without trying to set the value to something invalid. Let’s set the `my_user` kind to just UserKind. The compiler will tell us that it is looking for a value other than an Enum. Ok, that is good check. Let’s try a variant that doesn’t exist. Let’s set `my_user` kind to `UserKind::Invalid`. The compiler tells us there is no variant by that name for the Enum. Let’s define a new Enum and try a variant from it.

pub enum New {   Admin,}let my_user = User { kind: New::Admin };

The compiler will yell at us about mismatched types. This gives us a good idea about Enums and how they are first-class citizens.

There are a few different kinds of variants that can be declared. Above we’ve only used basic variants. Let’s take a look at a tuple variant. In simple terms, a tuple variant is a variant that takes a value.

Let’s add a score property to our User struct and its values will come from a new Enum. The variants will take a value representing either a high or low score for the user.

pub struct User {   kind: UserKind,   score: UserScore,}    #[derive(Debug)]pub enum UserScore {   HighScore(i8),   LowScore(i8),}let my_user = User {   kind: UserKind::Admin,   score: UserScore::HighScore(99),};let score = my_user.score;if let UserScore::HighScore(value) = score {   println!("value: {}", value);}

The first thing to notice is that our new Enum has two variants. A high score and a low score and they both expect an i8 value. We know from the [previous post on Numbers], that i8 is an 8-bit signed integer. When defining our `my_user` variable now, we also assign a score type of `UserScore::HighScore(99)`. This uses the HighScore variant of our UserScore Enum and assigns it a value of 99. Then our last three lines illustrate how to later pull that data back out by checking the score type against the HighScore variant with a value placeholder. The placeholder then holds the value we used in our assignment - `99`.

Now let’s talk about struct-like variants. When using tuple variants, things can get a little unorganized with multiple values being passed in. To better support that, Rust supports struct-like variants so we can name the various values being passed in like we do properties of a struct.

Let’s introduce a new Enum called `UserContact` that will hold either electronic or physical contact information. We’ll add a contact field to our User struct with a type of UserContact. Then assign our `my_user` an electronic type of the variant with two values.

pub struct User {   kind: UserKind,   score: UserScore,   contact: UserContact,}#[derive(Debug)]pub enum UserContact {   Electronic { email: String, text: String },   Physical { city: String, state: String },}let my_user = User {   kind: UserKind::Admin,   score: UserScore::HighScore(99),   contact: UserContact::Electronic {      email: String::from("user@example.com"),      text: String::from("1235131234"),   },};let contact = my_user.contact;if let UserContact::Electronic { email, text } = contact {   println!("email: {}, text: {}", email, text);}

Notice that the `UserContact` variables name the arguments (like we can do in Ruby method definitions). And in our `if let` statement on the third to last line, we compare our `my_user` contact value to a struct-like variant that includes the named fields of `email` and `text`. We can then use those values in our second to last line printing out the values.

To further illustrate, let’s accomplish the same thing with Ruby. We’ll make inline comments about any differences that are hard to reach parity on.

class User    USER_KINDS = [Admin, NonAdmin]    USER_SCORES = [HighScore, LowScore]    USER_CONTACTS = [Electronic, Physical]    attr_accessor :kind, :high_score, :low_score, :contact    # note: we are using class methods to obtain the allowed kinds    # one nicety about Rust enums is the compiler applying validation    # based on types    def self.admin      USER_KINDS.first    end    def self.nonadmin      USER_KINDS.last    end    # note: same as user kind, we are using class methods to return the allowed types    def self.high_score(value)      USER_SCORES.first.new(value)    end    def self.low_score(value)      USER_SCORES.last.new(value)    end    # note: we aren't reaching parity here compared to the Rust version    # in the Rust version, the user's contact is an enum and the variant determines the type    # in the Ruby version, we store either one as the contact    # we could reach parity by introducing a few more objects like a UserContact class     # that would return one or the other like the two enum variants in Rust    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  puts my_user.contact.email

You can see how we can achieve very similar things in both Ruby and Rust in the context of Enums, but the execution is quite a bit different. In the end, Ruby doesn’t have a first-class Enum citizen, so we have to create some of the functionality ourselves depending on the needs of our applications.

One thing we didn’t cover here is defining methods on an Enum. The good news is that you can do that Rust. But we’re going to leave implementing methods on objects for the next post where we dive into what Rust calls classes–Structs. We probably should have discussed Structs before Enums, but Enums illustrate a couple of differences between the languages.

Music companion of this post:
Ist Ist — Protagonists

What I am working on currently:
I am multitasking at the moment: expanding the API for my startup–Schemabook and CQRS style events going into Kafka inside a Rust app (converting from Rabbit to Kafka)