Published on 17th of Dezember 2017

Rust for Rubyists

— Idiomatic Patterns in Rust and Ruby

Recently I came across a delightful article on idiomatic Ruby. I'm not a good Ruby developer by any means, but I realized, that a lot of the patterns are also quite common in Rust. What follows is a side-by-side comparison of idiomatic code in both languages.

The Ruby code samples are from the original article.

Map

The first example is a pretty common iteration over elements of a container using map.

user_ids = users.map { |user| user.id }

The map concept is also pretty common in Rust. Compared to Ruby, we need to be a little more explicit here: If users is a vector of User objects, we first need to create an iterator from it:

let user_ids = users.iter().map(|user| user.id);

You might say that's quite verbose, but this additional abstraction allows us to express an important concept: will the iterator take ownership of the vector, or will it not?

The above Ruby code can be simplified like this:

user_ids = users.map(&:id)

Something similar can be done in Rust:

let user_ids = users.iter().map(id);

I'm cheating a little bit here because I omit a critical piece — creating a closure to extract the field from each item. It looks like this:

let id = |u: &User| u.id;

That's some more legwork course, but it's a nice trick and if you access the id more than once, it might pay off to define that closure.

Also note, that map() returns another iterator and not a collection. If you want a collection, you would have to run collect() on that, as we'll see later.

Each

Speaking of iteration, one pattern that I see a lot in Ruby code is this:

["Ruby", "Rust", "Python", "Cobol"].each do |lang|
  puts "Hello #{lang}!"
end

Since Rust 1.21, this is now also possible:

["Ruby", "Rust", "Python", "Cobol"]
    .iter()
    .for_each(|lang| println!("Hello {lang}!", lang = lang));

Although, more commonly one would write that as a normal for-loop in Rust:

for lang in ["Ruby", "Rust", "Python", "Cobol"].iter() {
    println!("Hello {lang}!", lang = lang);
}

Select and filter

Let's say you want to extract only even numbers from a collection in Ruby.

even_numbers = [1, 2, 3, 4, 5].map { |element| element if element.even? } # [ni, 2, nil, 4, nil]
even_numbers = even_numbers.compact # [2, 4]

In this example, before calling compact, our even_numbers array had nil entries. Well, in Rust there is no concept of nil or Null. You don't need a compact. Also, map doesn't take predicates. You would use filter for that:

let even_numbers = vec![1, 2, 3, 4, 5]
    .iter()
    .filter(|&element| element % 2 == 0);

or, to make a vector out of the result

// Result: [2, 4]
let even_numbers: Vec<i64> = vec![1, 2, 3, 4, 5]
    .into_iter()
    .filter(|element| element % 2 == 0).collect();

Some hints:

In Rust, there is no even method on numbers, but that doesn't keep us from defining one!

let even = |x: &i64| x % 2 == 0;
let even_numbers = vec![1, 2, 3, 4, 5].into_iter().filter(even);

In a real-world scenario, you would probably use a third-party package (crate) like num for numerical mathematics:

extern crate num;
use num::Integer;

fn main() {
    let even_numbers: Vec<i64> = vec![1, 2, 3, 4, 5]
        .into_iter()
        .filter(|x| x.is_even()).collect();
}

In general, it's quite common to use crates in Rust for functionality that is not in the standard lib. Part of the reason why this is so well accepted is, that cargo is such a rad package manager. (Maybe because it was built by no other than Yehuda Katz of Ruby fame. 😉)

As mentioned before, Rust does not have nil. However, there is still the concept of operations that can fail. The canonical type to express that is called Result.

Let's say you want to convert a vector of strings to integers.

let maybe_numbers = vec!["1", "2", "nah", "nope", "3"];
let numbers: Vec<_> = maybe_numbers
    .into_iter()
    .map(|i| i.parse::<u64>())
    .collect();

That looks nice, but maybe the output is a little unexpected. numbers will also contain the parsing errors:

[Ok(1), Ok(2), Err(ParseIntError { kind: InvalidDigit }), Err(ParseIntError { kind: InvalidDigit }), Ok(3)]

Sometimes you're just interested in the successful operations. An easy way to filter out the errors is by using filter_map:

let maybe_numbers = vec!["1", "2", "nah", "nope", "3"];
let numbers: Vec<_> = maybe_numbers
    .into_iter()
    .filter_map(|i| i.parse::<u64>().ok())
    .collect();

I changed two things here:

The return value contains all successfully converted strings:

[1, 2, 3]

The filter_map is similar to the select method in Ruby:

[1, 2, 3, 4, 5].select { |element| element.even? }

Random numbers

Here's how to get a random number from an array in Ruby:

[1, 2, 3].sample

That's quite nice and idiomatic! Compare that to Rust:

let mut rng = thread_rng();
rng.choose(&[1, 2, 3, 4, 5])

For the code to work, you need the rand crate. Click on the snippet for a running example.

There are some differences to Ruby. Namely, we need to be more explicit about what random number generator we want exactly. We decide for a lazily-initialized thread-local random number generator, seeded by the system. In this case, I'm using a slice instead of a vector. The main difference is, that the slice has a fixed size while the vector does not.

Within the standard library, Rust doesn't have a sample or choose method on the slice itself. That's a design decision: the core of the language is kept small to allow evolving the language in the future.

This doesn't mean that you cannot have a nicer implementation today. For instance, you could define a Choose trait and implement it for [T].

extern crate rand;
use rand::{thread_rng, Rng};

trait Choose<T> {
    fn choose(&self) -> Option<&T>;
}

impl<T> Choose<T> for [T] {
    fn choose(&self) -> Option<&T> {
        let mut rng = thread_rng();
        rng.choose(&self)
    }
}

This boilerplate could be put into a crate to make it reusable for others. With that, we get to a solution that rivals Ruby's elegance.

[1, 2, 4, 8, 16, 32].choose()

Implicit returns and expressions

Ruby methods automatically return the result of the last statement.

def get_user_ids(users)
  users.map(&:id)
end

Same for Rust. Note the missing semicolon.

fn get_user_ids(users: &[User]) -> Vec<u64> {
    users.iter().map(|user| user.id).collect()
}

But in Rust, this is just the beginning, because everything is an expression. This block splits a string into characters, removes the h, and returns the result as a HashSet. This HashSet will be assigned to x.

let x: HashSet<_> = {
    // Get unique chars of a word {'h', 'e', 'l', 'o'}
    let unique = "hello".chars();
    // filter out the 'h'
    unique.filter(|&char| char != 'h').collect()
};

Same works for conditions:

let x = if 1 > 0 { "absolutely!" } else { "no seriously" };

Although, you would usually use a match statement for that.

let x = match 1 > 0 {
    true => "absolutely!",
    false => "no seriously",
};

Multiple Assignments

In Ruby you can assign multiple values to variables in one step:

def values
  [1, 2, 3]
end

one, two, three = values

In Rust, you can only decompose tuples into tuples, but not a vector into a tuple for example. So this will work:

let (one, two, three) = (1, 2, 3);

But this won't:

let (one, two, three) = [1, 2, 3];
//    ^^^^^^^^^^^^^^^^^ expected array of 3 elements, found tuple

Neither will this:

let (one, two, three) = [1, 2, 3].iter().collect();
// a collection of type `(_, _, _)` cannot be built from an iterator over elements of type `&{integer}`

But with nightly Rust, you can now do this:

let [one, two, three] = [1, 2, 3];

On the other hand, there's a lot more you can do with destructuring apart from multiple assignments. You can write beautiful, ergonomic code using pattern syntax.

let x = 4;
let y = false;

match x {
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
}

This prints no since the if condition applies to the whole pattern 4 | 5 | 6, not only to the last value 6
— from The Book

String interpolation

programming_language = "Ruby"
"#{programming_language} is a beautiful programming language"

This can be translated like so:

let programming_language = "Rust";
format!("{} is also a beautiful programming language", programming_language);

Named arguments are also possible, but much less common:

println!("{language} is also a beautiful programming language", language="Rust");

The major difference is, that Rust is more leaning towards the C-style printf family of functions here.

That’s it!

The rest of the examples of the original article are quite Ruby-specific in my opinion. Therefore I left them out.

Ruby comes with syntactic sugar for many common usage patterns, which allows for very elegant code. Low-level programming and raw performance are no primary goals of the language.

If you do need that, Rust might be a good fit, because it provides fine-grained hardware control with comparable ergonomics. If in doubt, Rust favors explicitness, though. Rust eschews magic.

Did I wet your appetite for idiomatic Rust? Have a look at this Github project. I'd be thankful for contributions.