Acknowledgements

Introduction

I've been writing Ruby consistently since March 17, 2008. Why so specific? Well, that is the date the company I worked for hired a Cincinnati native to come talk to the engineers about a new framework called Rails.

That was essentially the day we switched all of our code repos from Subversion to Git. Why? Because that Cincinnati native was Chris Wanstrath, one of the co-founders of Github.

Learning Rails

Chris had just started working on GitHub but was still running the popular Err the Blog, which talked a lot about Ruby and Rails. The company I worked at was a design consultancy mostly doing work for Proctor & Gamble, but we had many clients. The variety of clients meant jumping back and forth between projects written in a potpourri of technologies and frameworks. One minute I was working in PHP, the next ASP (prior to .net) or Java. All of which were difficult to gain momentum with. Mostly due to the bespoke nature. But this Rails thing looked interesting! We had tinkered with it on our own but opted to bring in someone with more experience to verify our thoughts. Not only did Chris verify our thoughts, but we were granted early peeks and access to GitHub. Our development process changed a lot in those two days.

Since then, I have worked on a large number of Rails apps. I've written gems and contributed to open-source Ruby projects. I still do today, although lately, my time has been split. In the last year, I've been asked to work on several projects written in Rust. This is true at my day job, where the front end is a Rust app compiled to WebAssembly, and talks to multiple backend services written in Rust. I've escaped the browser and worked on several CLI tools written in Rust as well.

While learning Rust, I couldn't help but draw comparisons to Ruby. Mostly because it was the language I was most fluent in, but beyond that, there are similarities between the ecosystems. In the Getting Started chapter, I'll quickly cover the similarities. The similarities gave me comfort at first. It meant I would at least be able to get started without a steep learning curve. This book includes my approach and notes. I've written it to help other Rubyists who would like to learn Rust either for their day jobs or simply to learn.

The use cases for the two languages may be different and Rust doesn't have a framework that occupies the majority of the mindshare like Rails, but there are things from Ruby that can be applied to Rust code and vice versa. Rust opens up some new opportunities that can supplement Ruby code. In later chapters, we'll get into ways the two languages can be used together.

As always, thanks for reading!
Mike

Who should read this book

This book is written for those coming from the Ruby programming language. If you’re a Ruby programmer familiar with the fundamentals, then this book is for you!

But this isn’t the full picture! The Ruby examples in this book are likely to be relevant to any object-oriented programming language. There are some Ruby-specific comparisons, but the general approach still applies even if you’re coming from Java, Python, C#, or others.

This book is ideal for those with no Rust knowledge or just beginning their journey. It’s almost guaranteed that a Rustacean of any level will learn something new, but the majority of the content regards first steps with Rust.

In this book, you’ll find Rust code listings that walk you through examples by comparing them to similar Ruby code. We’ll cover every example and complete a project from start to finish.

And if you’re an experienced Rustacean reading this, maybe get a copy for your teammates.

About this book

This book is organized into two parts. Part I is focused entirely on the fundamentals of the Rust language and ecosystem.

We’ll learn how to read and write Rust code during this part of the book, which will be necessary in Part II. We’ll also cover slightly more advanced topics such as Ownership and the Stack vs Heap allocation. The code examples are all chapter-specific and will have nearly equivalent Ruby examples to learn through comparison.

Part II is where fundamentals are practiced. We’ll write a Ruby gem with native code in Rust. The purpose of Part II is to get familiar with writing Rust and understand one way Ruby and Rust can be used together.

About the code

Online resources

The examples and source code shown in this book can be found under the source code link at .

You’ll also find the sample application for Part II there. Please report any errors or suggestions using the errata link at .

If you like this book and it serves you well, I hope that you’ll let others know about it—your reviews help.

Tweets and posts are a great way to help spread the word. You can find me on LinkedIn[3] or you can tweet[4].

Mike Krisher July 2025

[1] https:// [2] https:// [3] https://www.linkedin.com/in/mkrisher/ [4] https://x.com/

Part I - Fundamentals of Rust

We’ll start with the fundamentals of the Rust language. You’ll learn the core syntax of Rust, the data types, and the memory-safe ownership design. You’ll get comfortable with compiling your code. We’ll compare all of the material to Ruby, so you’ll have a familiar foundation of understanding.

Chapter 1

Learning Rust by comparing to Ruby

Rust is continually on the annual lists of languages people love, and with good reason. It’s performant. Its compiler is developer-friendly. The community is approachable and accommodating. The ecosystem is mature. But it has a reputation for having a steep learning curve.

It’s hard to learn any new language, harder to become production-ready with it, and even harder still to convince your boss to actually let you use the new language in production. Rust opens a new way of thinking about programming that carries over into other languages as well.

Your knowledge of Ruby influences how you view and write in other programming languages. Similarly, as you develop an understanding of Rust, it will also influence how you think when writing code. Even if you were to never use Rust in production, you’ll still benefit and grow as a programmer.

We’ll start this chapter by taking a look at what makes Ruby such a great language. You’ll see why Rust is a similarly great language and why its future is bright. But before we talk about Rust, let’s talk about Ruby.

Appreciation for Ruby

The Case for Rust

Getting Started With Rust

When I first started learning Rust, it was important for me to learn how to set up my environment properly. That effort resulted in a blog post that sparked the idea for this book. I discovered there are many similarities between the Ruby and Rust ecosystems. Let’s get started.

With Ruby, you can install the runtime on your machine and begin. Many community members will use a version manager like rbenv. Rust doesn’t have an equivalent in that you switch between different versions by semantic version. There are toolchains, which can be thought of as versions, but I’ll get into those in a bit.

With Rust, you can be confident that developing on the latest stable version is the way to go. Before we get too ahead of ourselves, let’s install Rust.

The recommended way to install Rust locally is via rustup. Essentially, you are running a shell script that installs an executable–the Rust compiler.

  • Note: we didn’t simply say use homebrew or other package manager

Rustup gets us the latest “version” of the Rust compiler. Notice “version” is in quotes. Officially, Rust calls these toolchains. A toolchain is made up of a release channel and the compilation targets. If you are on a Mac, your compilation target is MacOS. There are three release channels. Nightly, beta, and stable. Nightly is the state of the main branch. It is work in progress. Possibly incomplete. Bleeding edge. Beta can be thought of as complete and ready for the community to test. Stable is an official release blessed by the community.

If you are developing a library to share with the community, you may want to test it against beta for example. You may want to have your tests run against all three release channels. But when in development, it probably makes most sense to use stable. You can switch between the release channels using: rustup default nightly for example. The equivalent in Ruby would be using your version manager to switch to a pre-release version of Ruby. As of this writing, a modern example would be Ruby 3.4.0-preview2. That would be similar to switching to the beta release channel of Rust.

You can see what version of Rust you have installed by asking the Rust compiler: rustc --version.

Side note: like using a .ruby-version file in your project, you can use a rust-toolchain file in your Rust project to specify the release channel you want to use.

Now that we have Rust installed and understand we’re going to use the “stable version”, let’s compare some of the ecosystem to Ruby.

Installation

The recommended way to install Rust locally is via rustup. Essentially, you are running a shell script that installs an executable–the Rust compiler.

  • Note: we didn’t simply say use homebrew or other package manager

Cargo

Cargo

If you have been using Ruby for any amount of time, you are familiar with Bundler, Gems, and Gemfiles. Rust has similar concepts.

The Bundler equivalent is called Cargo and is installed via rustup. Gems have an equivalent in Crates. Gemfiles have an equivalent in Cargo.toml files.

Cargo is the Rust package manager. It does more than just manage packages, but for now we’ll introduce it as the way you install the equivalent of Ruby Gems. You can see what version you have installed with: cargo --version.

For example, say we are going to be working with JSON. We would run cargo install serde. Serde is a serialization and deserialization library.

The Ruby equivalent would be bundle install serde. Not surprisingly, a Ruby Gem exists with that name but doesn’t offer the same functionality. That should give you a basic idea of how Cargo is the equivalent of Bundler.

Crates are the equivalent of Gems. They are libraries and executables you can install on your machine and use in your projects. Crates.io is the canonical source.

Cargo.toml is the equivalent of a Gemfile. It has a corresponding .lock file, just the same as your Ruby’s Gemfile. The toml file lists all of the packages you want to use and claim as dependencies. You can specify versions, paths, etc... Just like in a Gemfiles.

From a high level:

RustRuby
CargoBundler
CratesGems
Cargo.tomlGemfile

Now that you have a summary of the Rust equivalent of common Ruby items, let’s take a closer look at Cargo. Cargo offers a lot of functionality.

Cargo allows us to lint our code using cargo clippy. Cargo allows us to run the tests using cargo test. Cargo allows us to format our code using cargo fmt.

As alluded to earlier, it is more than a package manager. Bundler is as well, but Cargo has more functionality in the Rust context, especially when paired with the compiler. The compiler really is wonderful. It’s a shift compared to the runtime-ness of Ruby, but lean into it. Once you get into the flow of utilizing it, it really is quite helpful. Props to everyone that has made the Rust compiler friendly and useful. We’ll take a look at examples in future posts.

Let’s see some of this in action and along the way we’ll spell out the Ruby equivalent for comparison.

Your first Rust code

Summary

Chapter 2

New Language, New Syntax

Is Rust Object Oriented like Ruby?

Type System

Scalar Types

Numbers

Rust Numbers

In the last post, we discussed Strings. In this post, we are going to look at the differences between Ruby’s numbers and Rust’s various number types.

Numbers, either integers or floats, are pretty straight forward in Ruby. You simply declare the value and the interpreter will decide if it is an Integer or a Float. Example Ruby code below:

# integers
my_int = 4
puts my_int.class # => Integer

# floats
my_float = 4.2
puts my_float.class # => Float

If you are familiar with Ruby, you may take for granted how easy it is to declare a numeric data type and not really think about the differences between a whole integer and a float with a decimal. Not until you have to explicitly control precision maybe. Now let’s look at the Rust equivalent:

// integers
let my_int: i32 = 4;
println!("{}", my_int);

// floats
let my_float: f32 = 4.2;
println!("{}", my_float);

Woah! You may be asking what an i32 and f32 are. Rust is more explicit about numeric data declaration and it is wonderful. If you have written any heavy math in Ruby, you may have spent time figuring out where nulls were coming from, or why you were receiving Integers when you anticipated a Float. The Rust compiler will catch those differences and give clear and meaningful feedback. Let’s show an example:

// mismatch
let my_integer: i32 = 4.24;
println!("{}", my_integer);

If you try to run the above code with cargo run, you’ll receive the following feedback from the compiler:

--> src/main.rs:41:27
   |
41 |     let my_integer: i32 = 4.24;
   |                     ---   ^^^^ expected `i32`, found floating-point number
   |                     |
   |                     expected due to this

This is not unique to Rust, many typed languages can provide similar feedback, but in Ruby this would not be caught. Let’s embrace this further and dig into these types more.

Let’s discuss what an i32 is in more detail. It means a 32 bit integer. 32 bits means a range of numbers from -2,147,483,648 to 2,147,483,647. That’s a pretty big range.

There are related ranges. For example, an i8 is an 8 bit integer. It has a range of -128 to 127. Notice those ranges contain negative and positive numbers. If you perhaps know that you do not want to permit negative numbers, there is a u8 primitive type as well, stands for unsigned 8 bit number. It has a range of 0 to 255. Let’s see this action:

// small positive number
let my_pos_int: u8 = 255;
println!("{}", my_pos_int);

Easy enough, the output is 255. What happens if we try assigning a negative value:

error[E0600]: cannot apply unary operator `-` to type `u8`
  --> src/main.rs:45:26
   |
45 |     let my_pos_int: u8 = -255;
   |                          ^^^^ cannot apply unary operator `-`
   |
   = note: unsigned values cannot be negated

The compiler provides such great feedback. It immediately tells us that we have a variable assigned a u8 data type and we’ve tried assigning an invalid value. The compiler can really be useful. Let’s see what it says when we try to mutate our previous variable that is right at the maximum value of the u8 range.

// small positive number
let mut my_pos_int: u8 = 255;
println!("{}", my_pos_int);

// let's try going out of range
my_pos_int += 1;
println!("{}", my_pos_int);

This results in the process panicking, with the following error:

thread 'main' panicked at src/main.rs:49:5:
attempt to add with overflow

Notice that is not a compilation error. That is a runtime error. When we try to overflow the number to a value outside of the data types range, the process panics. We still have to be careful not to use invalid values at runtime.

  • NOTE: when a Rust process panics, it means the process exits and stops running.

So let’s get into some operators and see how basic arithmetic compares between Ruby and Rust.

let mut left: u8 = 1;
let right: u8 = 2;
let total: u8 = left + right;
println!("{}", total); // 3

let total: u8 = right - left;
println!("{}", total); // 1

left += 1; // increment
println!("{}", left);

left -= 1; // decrement
println!("{}", left);

let equality = left == right;
println!("{}", equality);

let float_addition: f32 = (left as f32 + 0.23 as f32) as f32;
println!("{}", float_addition);

let cast_addition: f32 = f32::from(left) + 0.23;
println!("{}", cast_addition);

Notice line 18 has some complexity. We’ll pull it out for better inspection:

let float_addition: f32 = (left as f32 + 0.23 as f32) as f32;

Is all of that really needed just to add an integer to a float? Well, yes, kinda. At least an understanding of what is required. Notice in the assignment (the left side) we declare the value as a type f32. So our intention is to have a 32-bit float value. On the right side, we have to cast the left value we declared prior as a f32 because its original value of u8 doesn’t have an add method that supports a float. So left as f32 casts the value to a float and then we add 0.23. It may look verbose, but if you’re doing rather long math routines, you’ll appreciate the compiler catching things for you.

That’s quite a bit different from Ruby but is a good illustration of the more explicitness we’ll find around numbers in Rust. We’ll stop there for now and go into Number data types and their operations in more detail in future chapters where we address common use cases.

Let’s move on to the next object in Rust, Enums.

Booleans

Character types

Strings

Let’s learn about Rust strings. As in plural. There are essentially two types. Um, what? You can’t be serious? Well, kinda.

Is this going to be one of those examples that demonstrates why Rust is hard to learn? No, not really. But, as simple as Strings are in other languages, they are a great gateway into the Rust ownership model, declaring types, stack vs heap, etc… Before we get ahead of ourselves, let’s see some simple examples compared to Ruby.

In Ruby, we can use Strings as values for variables and constants. We’re going to use the Ruby project created in the Getting Started chapter.

# lib/my_project_ruby.rb

module MyProjectRuby
  class Error < StandardError; end
  # Your code goes here...
  puts "hello world"
end

You can see we already use a String in the puts statement. The code “hello world” defines a String value. Let’s add some more examples.

# lib/my_project_ruby.rb

module MyProjectRuby
  class Error < StandardError; end
  
  NAME = "foo"
  puts NAME.class # => String

  substring = "this is a string".split(" ").last  
  puts substring.class # => String

  puts "hello world".class # => String
end

If you run this file via ruby lib/my_project_ruby.rb from the project root, you will see:

String
String
String

If you are familiar with Ruby, all of this should be straightforward. We defined a Constant with a String value. Next, we defined a variable called substring and used a method on a String to define another String value. Lastly, we used the existing puts statement, and rather than printing out the String value, we printed out the class of the value, which, of course, is String. Defining Strings in Ruby is pretty clear and obvious. You can use double or single quotes.

Let’s look at the equivalent in Rust. Again, we’ll reuse our Rust project from the Getting Started chapter.

// src/main.rs
const NAME: &str = "foo";

fn main() {
    println!("value of constant is {}", NAME);
    println!("Hello, world!");
}

Here we go! This is where we get into there being two String types in Rust. See how the constant is defined with a type of &str. This is the String literal in Rust. It is most commonly referred to as a string slice. And in its borrowed form. Slice? Borrowed? You may already be thinking that Rust Strings are a lot more complex than in a language like Ruby. Tis true. Let’s unpack this and get a good understanding.

String values can be defined in two ways in Rust. When defining a variable in Rust, actually called a binding, we use the let keyword. Our name for the variable follows. The value follows the equal sign operator. Declaring the data type is optional. Rust is good at inferring.

Briefly, inline comments in Rust use // rather than # in Ruby.

// this is a string slice type - read-only data
let hello_world = "Hello, World";

// this is a String type - pointer on stack or heap
let hello_world = String::from("Hello, world!");

// optionally we could declare the data types:
let hello_world: &str = "Hello, World";
let hello_world: String = String::from("Hello, world!");

str is defined here: https://doc.rust-lang.org/std/str/index.html.

String is defined here: https://doc.rust-lang.org/std/string/struct.String.html

Ok, why are there two String types? This is where we have to get into how Rust manages memory and performs so well. In most occasions, Rust manages memory using a Stack and a Heap. If you want a brief but concise overview, I suggest the Rust book. In the Ownership section there is a great break out section that describes the Stack vs the Heap. In addition to the Stack and Heap, there is a read-only data section in the program’s executable.

Notice in the inline comments I wrote read-only data and pointer on stack or heap. Read-only data in this sense means, we know the exact value at the time we define the value and when we compile our code. Our compiled executables have a read-only data section to store these values. This makes using the value extremely fast. The Rust book explains: “In the case of a string literal, we know the contents at compile time, so the text is hardcoded directly into the final executable. This is why string literals are fast and efficient.”

Let’s see what happens when we try to modify a string literal:

// src/main.rs

fn main() {
    let hello_world: &str = "Hello World";
    hello_world.push_str("! May I borrow you?");

    println!("{}", hello_world);
}

When we run this with cargo run, we receive the following error:

error[E0599]: no method named `push_str` found for reference `&str` in the current scope
 --> src/main.rs:8:17
  |
8 |     hello_world.push_str("! May I borrow you?");
  |                 ^^^^^^^^ method not found in `&str`

push_str is a method used to concatenate a String. Notice it doesn’t exist on &str. That is our first clue that &str values can not be changed. Not because it is a constant or frozen like we do in Ruby, but because the compiler is going to take the value at the time of assignment and add it to the data section of the executable.

In the second let above, we use String::from to define the value. Imagine we are collecting user input from a form. We don’t know the value the user would provide, so we take that input and use it to define a String. Because we don’t know the explicit and complete value at compile time, we use a String type and Rust stores this value on the Heap. This gives us a slot in memory that will be accommodating for various lengths of strings (with defined bounds). If we’re accepting user input, we may not know the number of characters they are going to enter, so we need variable space. Say something between 0 and 64 characters for example. Rust’s String struct contains an address, length and capacity. Capacity being the maxed allowed size. This is not something we have to consider with Ruby because the runtime manages the memory allocation for us (and the garbage collection).

Let’s see what happens when we try to grow that value:

// src/main.rs

fn main() {	
    let mut hello_world = String::from("Hello World");
    hello_world.push_str("!"); // add the exclamation

    println!("{}", hello_world);
}

This compiles and the result is: Hello World!

Notice we used mut to make the variable mutable, but the point isn’t about mutability. We are talking about growing the value in size.

Maybe we should think of them as hard coded vs dynamic values. In Ruby, NAME = "foo".freeze would be a &str because the value would be known, essentially hardcoded, should there be a compile step. Obviously, the same classes and objects don’t exist in both Ruby and Rust, so It’s not apples to apples. I’m simply using them to illustrate the point.

I was going to cover borrowing now since I mentioned it before, but it’s a topic all its own. For simple terms, let’s say borrowing means you reference values (and memory slots) rather than copying the value for a specific scope. We’ll cover it in a lot more detail in a future chapter.

So let’s stick to strings. Let’s complete this section with a look at how to do common things regardless of language. First a few common examples in Rust.

// src/main.rs
fn main() {
	# basic assignment
let hello_world = "Hello World";

// print a string
println!("{}", hello_world);

// concatenate a String
let mut name = String::from("Steve");
name.push_str(" Jobs");
println!("name: {}", &name);

// determine the length of a string (answer: 3)
let len = "foo".len();
println!("len: {}", &len);

// determine the index of a substring
let index: Vec<_> = "foobar".match_indices("bar").collect();
println!("index: {}", &index[0].0);

// trim newlines and spaces
let trim = " \nfoo \n ".trim();
println!("trimmed: {}", trim);

// debug/display trait
let debug_str = "this is a &str";
dbg!(debug_str);

let debug_string = String::from("this is a String");
dbg!(debug_string);
}

And now the equivalent in Ruby:

module MyProjectRuby
  class Error < StandardError; end
  
  # basic assignment
  hello_world = "hello world!"

  # print a string
  puts hello_world

  # concatenate a string
  name = "Steve";
  name += " Jobs";
  puts name

  # determine the length of a string (answer: 3)
  len = "foo".size
  puts "len: #{len}"

  # determine the index of a substring
  index = "foobar".index("bar")
  puts "index: #{index}"

  # trim newlines and spaces
  trim = " \nfoo \n ".strip();
  puts "trimmed: #{trim}"

  # debug/display trait
  debug_str = "this is a string";
  puts debug_str.inspect
end

That gives a basic overview of Strings in Rust compared to Ruby. In the next chapter, we’ll get into the various other common objects related to numbers.

References

Summary

Chapter 3

Variables

Control Flow

Options

Functions

Results

Ownership

Borrowing

Summary

Chapter 4

Data Types

Tuples

Arrays

Vectors

In the last chapter, we made a comparison between Rust Structs and Ruby Classes. Are they identical other than in name? No. But they do represent the same thing from a basic structure of code paradigm.

In this chapter, we are going to do a similar comparison between Rust’s Vectors and Ruby’s Arrays.

Vectors are integral to Rust. Strings are stored as Vectors. Vectors aren’t mutable by default. That’s a fun twist compared to Ruby’s Arrays.

Sidenote: Rust does have an Array collection type. It is fixed in size. Because of this, the community uses Vectors more often. But if you know the exact size, you may want to use the Array type for performance reasons.

Vectors, like Arrays, are collections. They hold elements. Typically of the same type. Though, sometimes we have to declare the type as we’ll see.

In our Ruby project, we have already declared a few arrays. However, let’s get explicit. In the Struct chapter, we set up a User class with kinds of users. Previously, our array contained Classes as elements. Let’s make this more simple by using Strings as the elements.

class User
	attr_accessor :kinds
	
	def initialize()
		@kinds = ['singer', 'guitarist']
	end
end

puts User.new().kinds #=> singer guitarist

The equivalent of that in Rust would be something like the following.

#![allow(unused)]
fn main() {
struct User<'a> {
	kinds: Vec<&'a str>
}

let user = User { kinds: vec!["singer", "guitaist"]};
println!("{:#?}", user.kinds); // => ['singer', 'guitarist']
}

Ignore the <‘a> for now on the Struct definition. That is related to lifetimes, which I’ll cover in a later chapter. For now, the important thing is the definition of a Vector.

In Ruby, we can declare some properties of our Array when we create it. For example, the length (of 3) with Array.new(3). We can also set the default value for elements with Array.new(3, 'foo').

With Rust, we can declare the type of elements the Vector will hold. For example, with String elements: Vec<String>. Or with Integer values: Vec<u8>.

#![allow(unused)]
fn main() {
// string elements 
let strings: Vec<String> = vec![String::from("foo"), String::from("bar")];

// integer elements 
let integers: Vec<u8> = vec![1,2];
}

But as mentioned previously, Rust Vectors are not mutable by default like Arrays are in Ruby. If we try to add an element to the integer example above, we’ll receive an error.

#![allow(unused)]
fn main() {
let integers: Vec<u8> = vec![1,2];
integers.push(3);

println!("{:#?}", integers);
}

On the first line, we declare our variable with a type of Vector. Then assign it a value of 1,2. On the next line, we try to add the value 3 to the Vector using the push method. The result is an error:

error[E0596]: cannot borrow `integers` as mutable, as it is not declared as mutable
   --> src/main.rs:183:5
    |
183 |     integers.push(3);
    |     ^^^^^^^^ cannot borrow as mutable
    |
help: consider changing this to be mutable
    |
182 |     let mut integers: Vec<u8> = vec![1,2];
    |         +++

The error explains pretty clearly what is happening. It also gives a solution. We can use the mut keyword to make our Vector mutable. Once we add the keyword, we can add the value of 3 to our Vector. With the exception of the mut keyword, so far the functionality is pretty close to Ruby’s Array.

In Ruby, we can use Arrays to store elements of multiple types. For example, we can store both Strings and Integers.

a = Array.new
a << 1
a << 'foo'

puts a.join(',') # => 1,foo

This can be done in Rust, but it requires declaring an Enum with the known types.

#![allow(unused)]
fn main() {
#[derive(Debug)]
enum MultiType {
	Integer(u8),
	Text(String),
}

let multi: Vec<MultiType> = vec![
	MultiType::Integer(1),
	MultiType::Text(String::from("foo"))
];
println!("{:#?}", multi);
}

That’s different and takes some time to wrap your head around when coming from Ruby. The other major thing that feels different about Rust Vectors compared to Ruby Arrays is how you enumerate over them.

In Ruby, Arrays are enumerable. We can do a loop over the values in multiple ways.

my_array = [1, 2, 3]
my_array.each do |num|
	puts num
end

In Rust, we have to make our Vector iterative which is similar in concept to an object being enumerable in Ruby.

#![allow(unused)]
fn main() {
let my_vector = vec![1,2,3];
my_vector.iter().for_each(|num| println!("{}", num));
}

But why do we have to make the Vector iterative first? Rust Vectors iterators are lazy. They don’t evaluate the element value until required. This allows us to combine multiple transformations over a single iteration of the data.

On the flipside of an iterator is a consumer. In the above example, we turn the Vector into an iterator and then chain on a consumer with the for_each method. Similar consumers include map, filter, and fold.

Before we get too deep into iterators and consumers, let’s look at more simple use cases for Vectors.

In Ruby, it’s easy to get to an element of a zero-index array using the bracket syntax.

my_array = [1, 2, 3]
puts my_array[0] # => 1

In Rust, we can do something similar.

#![allow(unused)]
fn main() {
let my_vector = vec![1,2,3];
println!("{}", my_vector[0]);
}

In Ruby, we can iterate over an Array and transform the values easily. We’ll use map! to transform the original array.

my_array = [1, 2, 3]
my_array.map! { |element| element + 1 }
puts my_array

In Rust, we have to consider mutability. In that case, we declare our Vector as mutable and use a different iterator called iter_mut.

#![allow(unused)]
fn main() {
let mut my_vector = vec![1,2,3];

for elem in my_vector.iter_mut() {
	*elem += 1;
}

println!("{:#?}", my_vector);
}

In the above example, we use a for loop to transform the existing Vector. This is considered the idiomatic way. But you may be wondering if we can use map like in the Ruby example. We can but we would have to reassign. We can use a for_each loop which would be similar to the Ruby example of map.

let mut new_vector = vec![4,5,6];
new_vector.iter_mut().for_each(|element| *element += 1);

println!("{:#?}", new_vector);

If we use map, we don’t mutate the existing Vector. Instead, we iterator over the existing Vector’s elements and then have to collect the results. Collect is an iterator consumer. The code looks like this:

let mut original_vector = vec![7,8,9];
let _ = original_vector
	.iter_mut()
	.map(|element| *element += 1)
	.collect::<Vec<()>>();

println!("{:#?}", original_vector);

Let’s break this down. There are a couple of things in this that breaks with our mental model of Ruby. First, we declare a mutable Vector. Next we declare a variable that we don’t plan on using, hence the _ name. Next we use a mutable iterator for the declared Vector. We then use the iterator and perform a map of the values. However, map doesn’t return the new values like we are used to in Ruby. We have to consume the values. That is what the collect method does. It tells the lazy map iteration that something is going to consume it. The collect method uses what is called the turbofish pattern to declare the type that is returned. In this case, we define the data type as a Vector of method calls. Those method calls mutate the original vector. Quite a bit different from Ruby and it’s map! method that mutates the caller.

Lastly, let’s filter a Vector. We do that pretty often in Ruby. For example:

words_array = %w[food fool found]
matches = words_array.map { |element| element if element.match?(/^foo/) }
puts matches

if statements in closures and ? boolean method names are some of my favorite design aspects of Ruby. Rust iterators and consumers are an interesting design aspect. The very convenient filter method on Vectors is an aspect that showcases that I feel. Let’s take a look.

let words_vector = vec!["barge", "barrel", "baltic"];
let matches = words_vector
	.iter()
	.filter(|element| element.starts_with("bar"))
	.collect::<Vec<_>>();

println!("{:#?}", matches);

I think most people looking at that Rust code would agree it’s pretty expressive. Understanding what it is doing is relatively straightforward.

There are a lot of similarities between Arrays and Vectors but as we’ve explored, the implementations are drastically different. Understanding the iterators and consumers patterns on Rust Vectors is the key to understanding how to use this powerful data type.

In the next chapter, we’re going to take a look at another collection data type that is central in a lot of Ruby code–Hashes. They’re called Hash Maps in Rust.

Keymaps

Keymaps

This is an important chapter for Rubyists. In Ruby, we use Hashes a lot. It’s our lookup table. It’s our key map. In Rust, similar functionality is called a Hash Map. It’s not used as much in Rust but as a Rubyist you will want to know how they compare.

The concept is the same. According to the Rust book, Hash maps are useful when you want to look up data not by using an index, as you can with vectors, but by using a key that can be of any type.

Let’s jump in.

Let’s use the Ruby project we created back in the Getting Started chapter and have made additions to since. We have the following code:

class User
  attr_accessor :kinds

  def initialize
	  @kinds = %w[singer guitarist]
  end
end

Rather than hardcoding the kinds of users in the constructor, let’s use a Hash as a lookup table and give the kinds of users a description.

class User
	KINDS = {
	  singer: 'leads the band, main vocalist, like Robert Smith',
	  guitarist: 'leads the rhythm, plays guitar, like Johnny Marr'
	}

	attr_accessor :kind

	def initialize(kind)
	  @kind = KINDS.fetch(kind, nil)
	end
end

user = User.new(:singer)
puts user.kind #=> "leads the band..."

KINDS in the above example is a common site in Ruby code. In this instance, we are declaring a constant with a Hash value. We then use the Hash to look up the value for a given key correlated to our User’s kind. This allows us to print out the value of the “kind” in the last line.

We can do something similar in Rust. Let’s see what that looks like.

use std::collections::HashMap;

pub struct User {
	kind: String,
}

impl User {
	pub fn new() -> Self {
		let binding = User::user_kinds();
    
		let kind = binding.get("singer").unwrap().to_string();

		Self { kind }
	}

	fn user_kinds() -> HashMap<String, String> {
		let mut kinds = HashMap::new();

		kinds.insert(
			String::from("singer"), 
			String::from("leads the band, main vocalist, like Robert Smith"));
		kinds.insert(
			String::from("guitarist"), 
			String::from("leads the rhythm, plays guitar, like Johnny Marr"));

		kinds
	}
}

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

There is a fair amount to unpack here. We tried to have a Rust example that mirrors the Ruby example, but Ruby makes it more convenient to use Hashes. Let’s break this down.

First, we have to import HashMap from the standard collections. Next, we declare our User Struct. Like the Ruby class, it has a single attribute named kind as a String data type.

Now we implement the methods on our User Struct. First is the new method. It’s pretty similar to the constructor in Ruby. It allows us to create an instance of our Struct. It returns the instance. While defining the instance, we assign the kind attribute to a value from our HashMap.

Now, the big difference between the Ruby example and the “trying really hard to be apples to apples” Rust example is how the HashMap is declared. In Ruby, we can declare the Hash as a constant in our class. There is no const constructor for a HashMap in Rust, due to the way the current implementation protects against HashDoS attacks. So our alternative is to declare the HashMap in an associated function.

  • reminder that an associated function in Rust is like a class method in Ruby.

With our associated function, our new method can reference the HashMap and we can assign our User’s kind attribute. We can see that the kind gets assigned a value in the last line where we print out the value.

In the above example, we use the get method of HashMap to retrieve the value for a specific key.

Let’s see what happens when the key doesn’t exist. Let’s change the line in the new method to look for a key other than singer.

let kind = binding.get("drummer").unwrap().to_string();

Because we have not defined a “drummer” key in our HashMap, if we run our program with cargo run we will receive a panic. This is probably not our desired result. We need to be more fault tolerant.

In Ruby, when working with Hashes we can set a default return if a key is not found. We did this in our constructor in the example above.

@kind = KINDS.fetch(kind, nil)

When using the fetch method to retrieve a value from a Hash by key, we can specify a second value that will be returned if the key being used is not found. In this case, we return nil. Rust is kind of doing the same thing.

When we use the get method in Rust, it returns an Option. We covered Option in the Enums chapter. Essentially, Option is an Enum that contains a Some or None value. Some means the key was found and returns the value. None means the key was not found. So why are we panicking?

Our panic is the result of our call to unwrap the result of our get call. In this case, we are looking for the key “drummer” which doesn’t exist and we receive an Option of None. We can’t unwrap none. But like in Ruby, we can make unwrap more fault tolerant.

We can be more fault tolerant to receiving None. If that happens we can unwrap or map the Some value or return a default value. Just like in Ruby. To do this, let’s change the line where we are looking up a value for “drummer” to the following:

let kind = binding.get("drummer").map_or("not found", |v| v).to_string();

Now when we run the program with cargo run we’ll receive “not found” for the user kind.

That gives us a decent comparison for using HashMaps in Rust compared to our heavy use of Hash in Ruby.

But we’re not done. Rust also provides a HashSet collection type. HashSet and HashMaps both implement the Hash trait, but they have different use cases which we’ll explore in the next chapter.

HashSets

In the last chapter, we compared Ruby’s Hash to Rust’s HashMap. In this chapter, we are going to take a look at another of Rust’s standard collections–HashSet.

The name can be confusing for Rubyists. Rust’s HashSet is like Ruby’s lesser-known class–Set. So Rubyists can kind of ignore the word Hash in the name.

It is pretty straightforward, let’s dig into some examples.

Summary

Chapter 5

Structure Types

Structs

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 chapter. Let’s stick to what a “class” looks like in Rust.

#![allow(unused)]
fn main() {
pub struct User {
	kind: &'static str,
}

impl User {
		// note rather than constant (with 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:

#![allow(unused)]
fn main() {
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 compilation 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 chapter, let’s talk about an Object that we use very very often–Vectors.

Enums

Rust Enums compared to Ruby objects

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 chapter, 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"
end

class NonAdmin
   NAME = "Application User"
end

class User
   USER_KINDS = [Admin, NonAdmin]
   attr_accessor :kind

   def self.admin
      USER_KINDS.first
   end

   def self.nonadmin
      USER_KINDS.last
   end
end

my_user = User.new
my_user.kind = User.admin # we could have used USER_KINDS.first

puts my_user.kind # prints out Admin
puts 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.

#![allow(unused)]
fn main() {
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 implementation 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.

#![allow(unused)]
fn main() {
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.

#![allow(unused)]
fn main() {
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 chapter 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. This is an example of pattern matching in Rust. 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.

#![allow(unused)]
fn main() {
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.

Generics

Lifetimes

Summary

Chapter 6

Code structure

Pattern Matching

Traits vs Mixins

Summary

Chapter 7

Errors

Panics

Summary

Chapter 8

Code helpers

Debugging

Tests

Logging

Compiler

Summary

Chapter 9

Project Structure

Modules

Libraries

Summary

Part 2 - Write a Ruby gem in Rust

Wrapping Rust in Ruby

Project Setup

Parsing URLs

Native Code compilation

Calling Rust from Ruby