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.