From Ruby to Rust: Implementing a Simple Class

I am primarily a Ruby developer, but I’ve heard a lot of good things about Rust and wanted to try it out. Ruby and Rust do have some similarities, but there are also some very big differences that I hope to highlight for you.

In this article we will implement a simple Ruby program in Rust. I will not only show you how to bridge the gap between the two languages but explain in detail how Rust is different than Ruby. I’m assuming that you have some programming experience and have also played with either Ruby or Rust yourself.

Let’s take a look at the Ruby code below…

class Person
attr_reader :first_name, :last_name

def initialize(first_name, last_name)
@first_name, @last_name = first_name, last_name
end
  def full_name
"#{first_name} #{last_name}"
end
end
me = Person.new("David", "Biehl")
puts "Hello, #{me.full_name}!"
puts "Shall I call you #{me.first_name}? Or would you prefer Mr. #{me.last_name}?"

We created a Person class that takes a first name and last name in the initializer. The Person class also implements a full_name method. Finally, we instantiate a new Person, say hello, and ask how they would prefer to be addressed.

Now let’s try to do this in Rust.

This first few examples won’t compile, but I’m going to walk through the steps to fix the code. Let’s take a naive attempt at a Rust implementation of this program.

// this won't compile... :(
struct Person {
first_name: String,
last_name: String,
}
impl Person {
// this is like a class method in Ruby, self is not in the
// args
fn new(first_name: String, last_name: String) -> Person {
Person {
first_name: first_name,
last_name: last_name,
}
}
    // this is like an instance method in Ruby, self is in the 
// args
fn full_name(self) -> String {
self.first_name + " " + self.last_name
}
}
fn main() {
let me = Person::new("John", "Doe");
    println!("Hello, {}!", me.full_name());
println!("Shall I call you {}? Or would you prefer Mr. {}?", me.first_name, me.last_name);
}

Looks pretty similar to the Ruby example, right? Well, unfortunately, this won’t compile. Let’s take a look at the errors one-by-one.

“Strings” aren’t always Strings

error: mismatched types [E0308]
let me = Person::new("John", "Doe");
^~~~~~~
help: run `rustc --explain E0308` to see a detailed explanation
note: expected type `std::string::String`
note: found type `&'static str`

error: mismatched types [E0308]
let me = Person::new("John", "Doe");
^~~~~~~
help: run `rustc --explain E0308` to see a detailed explanation
note: expected type `std::string::String`
note: found type `&'static str`

This is the same error, but it happened twice. Once for the string “John” and once for the string “Doe”. The error tells us that the Person::new method expects a type of std::string::String, but we’re sending it a &’static str. What’s the difference?

In Rust, string literals that are found in the code are compiled directly into the program and are referenced with a pointer. Hence the type &’static str type. So string literals are not a true String type in Rust. However, we can convert these into a String type by calling the to_string() method:

fn main() {
let me = Person::new("John".to_string(), "Doe".to_string());
println!("Hello, {}!", me.full_name());
}

Ah, that compiles. You can read more about the difference between the String and &’static str types in the Rust documentation: https://doc.rust-lang.org/book/strings.html

This highlights one of the biggest differences between Ruby and Rust: static typing. This means that method signatures in Rust require you to declare the types they accept for arguments and return values. The types are then checked when the program is compiled to make sure everything lines up correctly.

On to the next error!

String Concatenation isn’t Easy

error: mismatched types [E0308]
self.first_name + " " + self.last_name
^~~~~~~~~~~~~~
help: run `rustc --explain E0308` to see a detailed explanation
note: expected type `&str`
note: found type `std::string::String`

This is happening because Strings can’t simply be concatenated with the + operator, like Ruby. Looking at the String concatenation part of the Rust documentation, we find that references to Strings can be concatenated onto Strings (String + &str), so let’s try this instead:

fn full_name(self) -> String {
self.first_name + " " + &self.last_name
}

Ok, now that error is gone! You can read more about string concatenation in the Rust documentation: https://doc.rust-lang.org/book/strings.html#concatenation

One more error to look at, and this one is a bit tricky and will take us in a bit of a circle:

Ownership and Borrowing

error: use of moved value: `me.first_name` [E0382]
println!("Shall I call you {}? Or would you prefer Mr. {}?", me.first_name, me.last_name);
^~~~~~~~~~~~~
note: value moved here
println!("Hello, {}!", me.full_name());
^~
note: in this expansion of format_args!
note: in this expansion of print! (defined in <std macros>)
note: in this expansion of println! (defined in <std macros>)
help: run `rustc --explain E0382` to see a detailed explanation
note: move occurs because `me` has type `Person`, which does not implement the `Copy` trait

Ownership and Borrowing is a tricky subject in Rust, especially coming from Ruby. In Rust values are bound to bindings and there are rules regarding how this works.

Rust has a concept of ownership, and the first rule is that values can only be owned by a single binding at a time.

fn main() {
let me = Person::new("John".to_string(), "Doe".to_string());
// omit the rest
}

The let statement creates a binding called me that owns the Person value. As soon as me.first_name() is called, the ownership of the Person is being moved into the Person#first_name() method. In other words, the ownership of the person is transferred from the me binding to somewhere else. But where? A look at the full_name method definition gives us a clue.

fn full_name(self) -> String
// self is a new binding, and when me.full_name() is called the
// value is moved to self

We declared the full_name method with a binding called self. When me.full_name is called, the ownership of the Person value is transferred from the me binding to the self binding in the full_name method. In other words, me doesn’t own the Person anymore; ownership has been transferred to self inside of the full_name method.

So when we try to use the me binding after calling full_name, we get the error because me doesn’t own the value anymore. So how can we continue to use the me binding if we need it?

Rust has a concept called borrowing that allows the full_name method to borrow the Person value without transferring ownership. Let’s rewrite our full_name method so it borrows the value of Person instead by adding the borrow operator: &.

fn full_name(&self) -> String

Now we’re saying that when me.full_name is called, we’re not going to transfer ownership of the Person value, but we’re just going to let the full_name method borrow the value.

When we compile, the original error is gone, but this creates a new problem:

error: cannot move out of borrowed content [E0507]
self.first_name + " " + &self.last_name
^~~~
help: run `rustc --explain E0507` to see a detailed explanation

This is happening because we’re trying to transfer first_name, but we’ve only borrowed self. first_name is part of self and which we don’t currently own the value because we borrowed it. Therefore we can’t move anything that is a part of a value that has been borrowed. The compiler won’t let this happen. So let’s try to borrow self.first_name.

&self.first_name + " " + &self.last_name

Compile this … and, nope! New error…

error: binary operation `+` cannot be applied to type `&std::string::String` [E0369]
&self.first_name + " " + &self.last_name
^~~~~~~~~~~~~~~~
help: run `rustc --explain E0369` to see a detailed explanation
note: an implementation of `std::ops::Add` might be missing for `&std::string::String`
&self.first_name + " " + &self.last_name
^~~~~~~~~~~~~~~~

Now we’re OK with the borrow checker, and we’re back to type errors. For more information on ownership and borrowing the Rust documentation goes into great detail behind how this works and why it exists: https://doc.rust-lang.org/book/ownership.html

String Concatenation isn’t Easy: Part 2

So, I guess we can’t concatenate the &std::string::String type with the + operator. Bummer. Maybe we just need to re-think this. Let’s try something completely different:

fn full_name(&self) -> String {
let mut full_name = String::new();
    full_name.push_str(&self.first_name);
full_name.push_str(" ");
full_name.push_str(&self.last_name);
    full_name
}

First we create a mutable binding to an empty String called called full_name. Then we push the borrowed &self.first_name onto the String, push a space and then push a borrowed &self.last_name onto the String. Finally, the completed String is returned.

When you think about it, this is actually remarkably similar to what the Ruby code is doing behind the scenes. In both cases we are creating an entirely new instance of a String, only it’s much simpler in Ruby.

Let’s cross our fingers and compile … success! Now it works! Here is the final version of the Rust code.

// this works!
struct Person {
first_name: String,
last_name: String,
}
impl Person {
fn new(first_name: String, last_name: String) -> Person {
Person {
first_name: first_name,
last_name: last_name,
}
}
    fn full_name(&self) -> String {
let mut full_name = String::new();
        full_name.push_str(&self.first_name);
full_name.push_str(" ");
full_name.push_str(&self.last_name);
        full_name
}
}
fn main() {
let me = Person::new("John".to_string(), "Doe".to_string());
     println!("Hello, {}!", me.full_name());
println!("Shall I call you {}? Or should you prefer Mr. {}?", me.first_name, me.last_name);
}

Conclusion

We looked at several differences between Ruby and Rust, namely: type checking, working with strings and the concepts of ownership and borrowing. Now that you’ve read the examples, try and implement it yourself and let me know how it goes in the comments. I am new to Rust so if you are more experienced I would also love any feedback that you have on this example. Thanks for reading and have a great day!