About this ‘A Language a Day’ Advent Calendar
This article became a part of my book ‘A Language a Day’, which you can get in both electronic and paper format. Languages covered in the book: C++, Clojure, Crystal, D, Dart, Elixir, Factor, Go, Hack, Hy, Io, Julia, Kotlin, Lua, Mercury, Nim, OCaml, Raku, Rust, Scala, and TypeScript.
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.