Engin Diri
_CLOUD

_CLOUD

Learn Rust in under 10 mins

Learn Rust in under 10 mins

The impatient way

Engin Diri's photo
Engin Diri
·Oct 17, 2022·

8 min read

Introduction

How to increase your fluency in a programming language? You have to read a lot of it! Very lot. But how can you read a lot, when you lack the knowledge to understand what you read.

In this article, I try to cover as many keywords and symbols as possible using Rust snippets.

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

A semicolon marks the end of a statement:

let x = 3;
let y = 5;
let z = y + x;

This means that statements can span over multiple lines:

let x = vec![1, 2, 3, 4, 5, 6, 7, 8]
    .iter()
    .map(|x| x + 3)
    .fold(0, |x, y| x + y);

let introduce a new set of variables into the current scope

let x;
x = 42;

You can write this as a single line:

let x = 42;

Variables in Rust are immutable by default, and require the mut keyword to be made mutable.

let mut x = 42

Types can be annotated

let x: i32;
x = 42;

And again, you can write this as a single line

let x: i32 = 42;

When in doubt: Just use i32 for everything! i32 is the default in Rust

You can declare a name and initialize it later but you can not access uninitialized variables

let x;
foobar(x); // error: borrow of possibly-uninitialized variable: `x`
x = 42;

Doing it this way is completely fine:

let x;
x = 42;
foobar(x);

The underscore _ is a special symbol. It basically means that something is ignored:

// this does *nothing* because 42 is a constant
let _ = 42;

// this calls `get_thing` but throws away its result
let _ = get_thing();

Rust has tuples, which are a fixed-length collections of values of different types.

let pair = ('a', 17);
pair.0; // this is 'a'
pair.1; // this is 17

We could write, with explicit type annotation:

let pair: (char, i32) = ('a', 17);

Tuples can be destructured when doing an assignment:

let (some_char, some_int) = ('a', 17);
// now, `some_char` is 'a', and `some_int` is 17

This is very useful when a function returns a tuple:

let (left, right) = slice.split_at(middle);

You can use underscore _ to throw away a part of it:

let (_, right) = slice.split_at(middle);

To declare a function, you use fn.

Here is a void function:

fn greet() {
    println!("Hi there!");
}

And this is a function that returns a 32-bit signed integer. The arrow indicates its return type!

fn fair_dice_roll() -> i32 {
    4
}

Functions can be generic:

fn foobar<T>(arg: T) {
    // do something with `arg`
}

You can have multiple type parameters:

fn foobar<L, R>(left: L, right: R) {
    // do something with `left` and `right`
}

Bracket pairs do declare a block. A block has its own scope:

// This prints "in", then "out"
fn main() {
    let x = "out";
    {
        // this is a different `x`
        let x = "in";
        println!("{}", x);
    }
    println!("{}", x);
}

Keep in mind that Blocks are also expressions. This means they evaluate to a value.

// this:
let x = 42;

// is equivalent to this:
let x = { 42 };

There can be multiple statements inside a block:

let x = {
    let y = 1; // first statement
    let z = 2; // second statement
    y + z // this is the *tail* - what the whole block will evaluate to
};

When you omit the semicolon at the end of a function, it is the same as returning:

fn fair_dice_roll() -> i32 {
    return 4;
}

fn fair_dice_roll() -> i32 {
    4
}

if conditions are expressions too:

fn fair_dice_roll() -> i32 {
    if feeling_lucky {
        6
    } else {
        4
    }
}

And a match is also an expression and not a statement!

fn fair_dice_roll() -> i32 {
    match feeling_lucky {
        true => 6,
        false => 4,
    }
}

Dots . are used to access fields of a value:

let a = (10, 20);
a.0; // this is 10

let amos = get_some_struct();
amos.nickname; // this is "fasterthanlime"

Or to call a method on a value:

let nick = "fasterthanlime";
nick.len(); // this is 14

The double-colon, ::, is similar, but it operates on namespaces.

In this example, std is a crate (~ a library), cmp is a module (~ a source file), and min is a function:

let least = std::cmp::min(3, 8); // this is 3

A Struct is declared with the struct keyword

struct Number {
    odd: bool,
    value: i32,
}

And they can be initialized using literals

let one = Number { odd: true, value: 1 };
let two = Number { odd: false, value: 2 };

You can declare methods on your own types:

impl Number {
    fn is_strictly_positive(self) -> bool {
        self.value > 0
    }
}

Structs can be generic too:

struct Pair<T> {
    a: T,
    b: T,
}

fn print_type_name<T>(_val: &T) {
    println!("{}", std::any::type_name::<T>());
}

fn main() {
    let p1 = Pair { a: 3, b: 9 };
    let p2 = Pair { a: true, b: false };
    print_type_name(&p1); // prints "Pair<i32>"
    print_type_name(&p2); // prints "Pair<bool>"
}

A vector is a container that stores the values like an array, but it has more advantages than an array data structure. A vector can increase it's size dynamically during runtime.

It is provided by the standard library and is a generic.

fn main() {
    let mut v1 = Vec::new();
    v1.push(1);
    let mut v2 = Vec::new();
    v2.push(false);
    print_type_name(&v1); // prints "Vec<i32>"
    print_type_name(&v2); // prints "Vec<bool>"
}

While we talked about Vectors, let's check the vec! macro:

fn main() {
    let v1 = vec![1, 2, 3];
    let v2 = vec![true, false, true];
    print_type_name(&v1); // prints "Vec<i32>"
    print_type_name(&v2); // prints "Vec<bool>"
}

All types of name!(), name![] or name!{} are invoking a macro. Macros just expand to regular code.

You already know a famous macro: println!

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

This macro expands to:

fn main() {
    use std::io::{self, Write};
    io::stdout().lock().write_all(b"Hello there!\n").unwrap();
}

Rust provides a loop keyword to indicate an infinite loop.

loop {
        count += 1;

        if count == 3 {
            println!("three");

            // Skip the rest of this iteration
            continue;
        }

        println!("{}", count);

        if count == 5 {
            println!("OK, that's enough");

            // Exit this loop
            break;
        }
    }

Or you can use the while loop. As the name stated, this loops runs while a condition is true.

fn main() {
    // A counter variable
    let mut n = 1;

    // Loop while `n` is less than 101
    while n < 101 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }

        // Increment counter
        n += 1;
    }
}

Anything that is iterable can be used in a for in loop. One of the easiest ways to create an iterator is to use the range notation a..b.

fn main() {
    // `n` will take the values: 1, 2, ..., 100 in each iteration
    for n in 1..101 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }
    }
}

But for in it also works with a Vec:

fn main() {
    for i in vec![52, 49, 21] {
        println!("I like the number {}", i);
    }
}

Or a slice:

fn main() {
    for i in &[52, 49, 21] {
        println!("I like the number {}", i);
    }
}

Or an actual iterator:

fn main() {
    // note: `&str` also has a `.bytes()` iterator.
    // Rust's `char` type is a "Unicode scalar value"
    for c in "rust".chars() {
        println!("Give me a {}", c);
    }
}

You can catch the failure of some parts of your program instead of calling panic!. This can be done using the Option enum.

The Option<T> enum has two variants:

  • None, to indicate failure or lack of value, and
  • Some(value), a tuple struct that wraps a value with type T.
enum Option<T> {
    None,
    Some(T),
}

impl<T> Option<T> {
    fn unwrap(self) -> T {
        // enums variants can be used in patterns:
        match self {
            Self::Some(t) => t,
            Self::None => panic!(".unwrap() called on a None option"),
        }
    }
}

use self::Option::{None, Some};

fn main() {
    let o1: Option<i32> = Some(128);
    o1.unwrap(); // this is fine

    let o2: Option<i32> = None;
    o2.unwrap(); // this panics!
}

Sometimes it is important to display why an operation failed. To do this you can use the Result enum.

The Result<T, E> enum has two variants:

  • Ok(value) which indicates that the operation succeeded
  • Err(why), which indicates that the operation failed, and wraps why, which explains the cause of the failure.
use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => println!("Problem opening the file: {:?}", error),
    };
}

Further reading

I hope, I could give a got overview about Rust in under 10 mins. Of course, this is just the beginning of a wonderful journey in the word of Rust. For more Rust material, check out the following links:

  1. doc.rust-lang.org/rust-by-example/index.html
  2. doc.rust-lang.org/book/title-page.html
  3. runrust.miraheze.org/wiki/Main_Page
  4. gist.github.com/rylev/f76b8e9567a722f1b157a..
  5. learnxinyminutes.com/docs/rust
 
Share this