Rust at a Glance — A Language a Day, Advent Calendar 2019 Day 2/24

About this ‘A Language a Day’ Advent Calendar 2019

Welcome to Day 2 of this year’s A Language a Day Advent Calendar. Today’s topic is introduction to the Rust programming language.

Facts about the language

Some facts about Rust:

  • Syntactically close to C++
  • Makes memory management safe
  • It is a compiled language
  • Object-orientation is based on traits
  • Introduced in 2010
  • Website: www.rust-lang.org

Installing and Running Rust

To install your Rust compiler, follow the instructions from its website and run:

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

After the bash script finishes its work, you get the ~/.cargo/bin directory that contains a few executables, including the Rust compiler, rustc. The installer also modifies the $PATH variable via .profile. Among the other tools, you will find the cargo executable, which is a package manager.

After the installation, you can also run a local version of the documentation, the following command opens it in a browser:

$ rustup doc

To run a program, you need to compile it. You can do it using the cargo tool or directly with the command-line compiler rustc:

$ rustc helloworld.rs
$ ./helloworld

After the compilation you get a binary executable.

Hello, World!

Here is the minimum program in Rust:

fn main() {
    println!("Hello, World!");
}

You can immediately notice two things here: first, the resemblance to the C and C++ code with their main function, and second, the shortened keyword fn for declaring a function.

Please also note that the println! name ends with an exclamation mark. This is because it is a macro, not a function. We’ll have a simple macro example later on this page.

Variables

Variables are introduced using the let keyword. You can define a variable and initialise it in one go:

let name = "John";

This is called variable binding.

It is important to realise that variables created like the above are immutable. Although you can (in theory, but you will get a warning) assign an initial value on a separate line of code, you cannot re-assign it:

fn main() {
    let name;
    name = "John";  // OK
    name = "Alice"; // Error
                    // "cannot assign twice to immutable variable"
}

To make a variable mutable, add the mut keyword:

fn main() {
    let mut name2;
    name2 = "John";  // OK
    name2 = "Alice"; // OK with warning
                     // "maybe it is overwritten before being read?"
}

From the warning message, you can see how carefully Rust handles variable usage.

To force a type and avoid type deducing, add a type yourself:

fn main() {
    let x: i8 = 100;
    let y: i32 = 65537;

    println!("Size of x: {}", std::mem::size_of_val(&x)); // 1
    println!("Size of y: {}", std::mem::size_of_val(&y)); // 4
}

Here, the compiler will also check if the value you are binding to a variable fits the range associated with the given type. Notice how you pass a variable by reference in the println! calls.

Functions

We have already seen the simplest variation of a function that takes no arguments and returns no result. Let us add them in the following example.

A Factorial example

fn main() {
    println!("5! = {}", factorial(5));
}

fn factorial(n: u32-> u32 {
    return (1 ..= n).product();
}

The factorial function takes and returns the values of the u32 (unsigned 32-bit integer) type. The type of the argument is said after the colon similar to how we did it with the let keyword. The return type is stated after the -> arrow.

Also note that the factorial function is defined after the main function, where it is used. This is totally accepted by Rust.

Macros

We’ve seen an example of a useful macro in the Hello, World! example already. Let’s have a look at how you can define your own.

A macro is a set of rules that lead to a certain code execution. Consider the following definition of a my_macro macro:

macro_rules! my_macro {
    ($x:ident) => (println!("identifier"));
    ($x:expr)  => (println!("expression"));
}

fn main() {
    my_macro!(10); // expression

    let n = 10;
    my_macro!(n); // identifier
}

The body of my_macro definition contains two distinguishable rules. The first line is triggered when the macro gets an identifier (thus, :ident) as its argument. The second line works for expressions.

In the main function, you see two macro calls. First, you pass a constant integer, then a variable. Marco prints the message corresponding to the filter matched.

The $-prefixed thing is what will be substituted when the macro is expanded. Consider the updated example. It is quite artificial, but I hope it clearly demonstrates how a definition of a macro works.

macro_rules! my_macro {
    ($x:ident) => (let $x = 42;);
    ($x:expr)  => (println!("value = {}", $x));
}

fn main() {
    my_macro!(n);
    my_macro!((n)); // or: my_macro!(n + 1)
    println!(n);
}

Here, the macro is called first with an identifier, which was not defined earlier in the main function. In the second call, it is clearly an expression (or you may replace it with, say, n + 1, to make it a more obvious expression.

When macro has been enrolled, the first call becomes a piece of code for creating and initialising the $x variable, where $x is replaced with n: let n = 42. The second call is replaced with the printing instruction and outputs value = 42. Or value = 43 if you call it as my_macro!(x + 1). You can also use the new variable in the rest of the main function.

Object-oriented features

Rust’s understanding of object-oriented programming differs from traditional approaches, both in syntax and philosophy.

Let us start with a simple ‘class’ for keeping the two pieces of personal data: name and age.

struct Person {
    name: String,
    age: u32,
}

impl Person {
    fn new(n: String, a: u32) -> Person {
        return Person {name: n, age: a};
    }

    fn info(&self) -> () {
        println!("Name: {}, age: {}.", self.name, self.age);
    }
}

fn main() {
    let john = Person::new("John".to_string(), 32);
    john.info();
}

Here, the data structure and the methods to work with them are defined in two different places: as a struct and as an implementation (impl). Both parts share the same name Person.

Inside the implementation, you can see two functions. The new method returns a newly created instance. This method does not reference to any existing instance, and it is thus a static method. The other one, info, needs a reference &self to an existing object. The data fields are accessed using the dot notation, for example, self.name.

Also notice the return type of these two methods. The new function returns a Person structure, and the info method returns nothing, which can be expresses either by omitting the return type or by explicitly saying -> (). To call a method from outside the class, use double colons: Person::new(...).

A polymorphic example

The next example demonstrates how you can organise what you would normally call class hierarchy, but in Rust it is an interface hierarchy.

We will create a few objects of two types: dogs and cats and put them to the same vector, which only knows that the objects implement the same trait.

The impl Speak for Cat construct defines the behaviour that you expect from a Speak-able Cat object. Its general shape is articulated by the trait definition.

struct Cat {}
struct Dog {}

trait Speak {
    fn speak(&self) -> ();
}

impl Speak for Cat {
    fn speak(&self) {
        println!("Cat");
    }
}

impl Speak for Dog {
    fn speak(&self) {
        println!("Dog");
    }
}

fn main() {
    let cat1 = Cat {};
    let cat2 = Cat {};
    let dog = Dog {};

    let zoo: Vec<&Speak> = vec![&cat1, &dog, &cat2];
    for x in zoo {
        x.speak();
    }
}

The program prints Cat, Dog, Cat as expected.

Error handling

When you divide by zero, you can have different outcome depending on what data type you are woking with.

With the floating-point numbers, you get an infinity, and no errors occur:

fn main() {
    let z = 0.0;
    let x = 42.0 / z;
    println!("Result = {}", x); // Result = inf
}

Modify the very same program to work with integers, and you get a runtime exception:

fn main() {
    let z = 0;
    let x = 43 / z;
    println!("Result = {}", x); // panic
}

File not found

This is how you can catch an attempt of opening a non-existing file.

use std::fs::File;

fn main() {
    let response = match File::open("nofile.txt") {
        Ok(_) => "200 OK",
        Err(_) => "404 Not Found",
    };
    println!("{}", response);
}

The match expression together with the two patters, Ok and Err, form a construct that handles both cases: whether the file exists or it is not found. The underscore here represents a dummy argument, which is required but not used. Compare the branch handling syntax with how macros dispatch things based on the types of their argument.

Concurrency

Rust offers support for working with threads using channels and child processes or threads. For a quick demo, let us write a program that implements the Sleep Sort algorithm using native threads.

Sleep Sort

The following program sorts a vector of integers saved in the data variable. For each value, a separate thread is spawned. The new threads are collected in the threads vector. After all data items are processed, the main code is waiting for all of them to be completed (joined).

use std::thread;

fn main() {
    let data = vec![10, 4, 2, 6, 2, 7, 1, 3];
    let mut threads = vec![];

    for n in data {
        threads.push(thread::spawn(move || {
            thread::sleep_ms(n * 10u32);
            println!("{}", n);
        }));
    }

    for child in threads {
        child.join();
    }
}

Inside the threads, a delay is introduced, and the sorted element defines the duration of the delay. Notice how the data is converted to a u32 value by multiplying it by 10u32 to make the delay a bit longer to make the code more robust.

Get more

That’s all for today, but you are invited to dig Rust more. Use the following resources for the start:

I hope that my brief overview gave you the feeling of Rust being a very interesting language with a big focus of type safety. Together with that, it is a compiled language that produces executables running at machine native speed.

You can find all the source codes that were used in this article about Rust on GitHub.

Next: Day 3. Julia at a glance.

Leave a Reply