Rust 

Part 2

Syntax/Semantics

Variable Binding

fn main() {
    let x = 5;
}
let (x, y) = (1, 2);
let x: i32 = 5;

Immutable by default

let x = 5;
x = 10; // will error on compile
let mut x = 5;
x = 10

Primary Reason: Safety

If you forgot to say mut the compiler will catch it, and let you know that you have mutated something you may not have intended to mutate. 

If bindings were mutable by default, the compiler would not be able to tell you this. 
If you did intend mutation, then the solution is quite easy: add mut.

Initializing Bindings

fn main() {
    let x: i32;
}
fn main() {
    let x: i32;
    let y = x + 5; // error
}

TL;DR bindings are required to be initialized with a value before you're allowed to use them.

Scope

fn main() {
    let x: i32 = 17;
    {
        let y: i32 = 3;
    }
    let z = x + y // error
}
fn main() {
    let x: i32 = 8;
    {
        // x is 8
        let x = 12;
        // x is 12
    }
    // x is 8
}

TL;DR Variable bindings have a scope - they are constrained to live in a block they were defined in.

Functions

fn foo() {

}
fn do_something(x: i32) {
   
}
fn inc(x: i32) -> i32 {
    x + 1
}

 you must declare the types of function arguments.

fn inc(x: i32) -> i32 {
    return x + 1; // early return

    x + 2 // never reached
}

Function pointers

let f: fn(i32) -> i32;
// without type inference
let f: fn(i32) -> i32 = inc;

// with type inference
let f = inc;

let six = f(5);
fn inc(x: i32) -> i32 {
    x + 1
}

Primitive Types

let x = true;

let y: bool = false;
let x: char = 'x';
let two_hearts = '💕';

Booleans

char ( unicode )

Numeric types

let x: i8 = 3;

let x = 42; // x has type i32

let y = 1.0; // y has type f64

i8, i16, i32, i64,

Signed fixed size Integer types

u8, u16, u32, u64,

Unsigned fixed size Integer types

isize, usize

Variable sized types (Size dependent of the pointer size of the underlying machine)

f32, f64

Floating point types (IEEE-754 single and double)

Arrays

let a = [1, 2, 3]; 

let mut b: [i32; 3] = [1,2,3];

let c = [0; 20]; // initialize each element to 0
let a = [1, 2, 3]; 

let len = a.len()

let three = a[2];

Slices

let a = [0, 1, 2, 3, 4];
let complete = &a[..]; // A slice containing all of the elements in a
let middle = &a[1..4]; // A slice of a: only the elements 1, 2, and 3

A ‘slice’ is a reference to (or “view” into) another data structure. They are useful for allowing safe, efficient access to a portion of an array without copying.

Reference

I'll cover later

Tuples

let x = (1, false);
let x: (i32, bool) = (1, true);
let a = (1, 2, 3);

let (x, y, z) = a

let (x, y, z) = (1, 2, 3);
let tuple = (1, 2, 3);

let x = tuple.0;
let y = tuple.1;
let z = tuple.2;

If

let x = 5;

if x == 5 {
    do_something();
} else {
    do_something_else();
}
let x = 5;

if x == 5 {
    do_something();
} else if x == 6 {
    do_something();
}
let x = 5;
let y = if x == 5 { 10 } else { 15 };

Loops

loop {
    do_something(); //repeat forever
}
let mut x = 5;
let mut done = false;

while !done {
    x += 2;
    if x % 5 == 0 {
        done = true;
    }
}
for (i,j) in (5..10).enumerate() {
    do_something(i, j);
}
let lines = "hello\nworld".lines();

for (linenumber, line) in lines.enumerate() {
    do_something(line_number, line);
}

Structs

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let origin = Point { x: 0, y: 0 }; 
    println!("The origin is at ({}, {})", origin.x, origin.y);
}
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let mut point = Point { x: 0, y: 0 };

    point.x = 5;

    println!("The point is at ({}, {})", point.x, point.y);
}
struct Point3d {
    x: i32,
    y: i32,
    z: i32,
}

let mut point = Point3d { x: 0, y: 0, z: 0 };
point = Point3d { y: 1, .. point };

Structs

struct Colour(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Colour(0, 0, 0);
let origin = Point(0, 0, 0);
struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn area(&self) -> f64 {
        3.14 * (self.radius * self.radius)
    }
}

fn main() {
    let c = Circle { x: 0.0, y: 0.0, radius: 2.0 };
    println!("{}", c.area());
}

Enums

enum Message {
    Quit,
    ChangeColor(i32, i32, i32),
    Move { x: i32, y: i32 },
    Write(String),
}
let x: Message = Message::Move { x: 3, y: 4 };
let v = vec!["Hello".to_string(), "World".to_string()];

let v1 = v.into_iter().map(Message::Write).collect();
// v1 is a vec of [Message::Write("Hello"), Message::Write("World")]

Enum constructor example.

Match

let number = 3
let size = match number {

	0 => "none",
	2 | 3 => "tiny",
	4...7 => "small",	
	8...20 => "medium",
	_ => "large"
};
enum Message {
    Quit,
    ChangeColor(i32, i32, i32),
    Move { x: i32, y: i32 },
    Write(String),
}

fn quit() { /* ... */ }
fn change_color(r: i32, g: i32, b: i32) { /* ... */ }
fn move_cursor(x: i32, y: i32) { /* ... */ }

fn process_message(msg: Message) {
    match msg {
        Message::Quit => quit(),
        Message::ChangeColor(r, g, b) => change_color(r, g, b),
        Message::Move { x: x, y: y } => move_cursor(x, y),
        Message::Write(s) => println!("{}", s),
    };
}

 The Rust compiler checks exhaustiveness, so it demands that you have a match arm for every variant of the enum. If you leave one off, it will give you a compile-time error unless you use _ or provide all possible arms.

Match

let pair = (1,2);
let smaller = match pair {
	(x, y) if x < y => x,
	(_, y) => y
};
match point {
	Point {x: 2...6, y: -1...5} => println!("I like this point"),
	_ => println!("I do not like this point")
}

Generics

enum Option<T> {
    Some(T),
    None,
}

let x: Option<i32> = Some(5);
fn takes_anything<T>(x: T) {
    // do something with x
}

fn takes_two_of_the_same_things<T>(x: T, y: T) {
    // ...
}

fn takes_two_things<T, U>(x: T, y: U) {
    // ...
}

struct Point<T> {
    x: T,
    y: T,
}

let int_origin = Point { x: 0, y: 0 };
let float_origin = Point { x: 0.0, y: 0.0 };

Traits

trait Pointy {
        fn poke(&self, at: &str);
        // One can also give full function bodies here
}

impl Pointy for Point {
        fn poke(&self, at: &str) {
                println!("Poked {}", at);
        }
}

fn poke_forever<T: Pointy>(pointy: T, at: &str) {
        loop {
         pointy.poke(at)
        }
}

Core Concepts

Ownership

Variable bindings have ownership of what they're bound to.

let v = vec![1, 2, 3];

let v2 = v;

println!("v[0] is: {}", v[0]); // error
fn take(v: Vec<i32>) {
    // what happens here isn’t important.
}

let v = vec![1, 2, 3];

take(v);

println!("v[0] is: {}", v[0]); // error

Move semantics

fn helper(value: Vec<i32>) {
    println!("The list was: {}", value);
}

fn main() {
    let list = vec![1,2,3];
    
    helper(list); // move by default
    helper(list); // error
}
fn helper(value: Vec<i32>) -> Vec<i32> {
    println!("The list was: {}", value);
    value
}

fn main() {
    let list = vec![1,2,3];
   
    let a = helper(list); // transfer ownership and acquire ownership of return value
    
}

Why

Rust allocates memory for an integer on the stack, copies the bit pattern representing the value of 10 to the allocated memory and binds the variable name x to this memory region for future reference.

let x = 10;

Rust allocates memory for the vector object v on the stack like it does for x above. But it also allocates some memory on the heap for the actual data ([1, 2, 3]).

 

Rust then copies the address of this heap allocation to a pointer in the  vector object placed on the stack.

let v = vec![1, 2, 3];

Why

When we move v to v2 Rust does a bitwise copy of the vector object representing v into the stack allocation represented by v2.

This shallow copy does not actually create a copy of the heap allocation containing the actual data.

So now there are two pointers pointing to the same memory allocation on the heap. 

let v = vec![1, 2, 3];

let mut v2 = v;

Why

 and v were still accessible we'd end up with an invalid vector since v would not know that the heap data has been truncated. 

Now vector v on the stack does not agree with the corresponding part on the heap.

v still thinks there are three elements in the vector and will happily let us access the non existent element 

It’s also important to note that optimizations may remove the actual copy of the bytes on the stack, depending on circumstances. So it may not be as inefficient as it initially sounds.

 

v2.truncate(2);

Now for example if we truncated the vector to just two elements through v2:

int main() {
    int *slot = malloc(sizeof(int));
    *slot = 3;
    helper(slot);
    helper(slot); // use after free!
}

void helper(int *slot) {
    printf("The number was: %d\n", *slot);
    free(slot);
}
The number was: 3
The number was: 0
*** Error in `./a.out': double free or corruption (fasttop): 0x00000000024f9010 ***
======= Backtrace: =========
/usr/lib/libc.so.6(+0x6f364)[0x7fc0de654364]
/usr/lib/libc.so.6(+0x74d96)[0x7fc0de659d96]
/usr/lib/libc.so.6(+0x7557e)[0x7fc0de65a57e]
./a.out[0x4005ee]
./a.out[0x4005b8]
/usr/lib/libc.so.6(__libc_start_main+0xf0)[0x7fc0de605710]
./a.out[0x4004a9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 08:02 5058341                            /home/anthony/a.out
00600000-00601000 rw-p 00000000 08:02 5058341                            /home/anthony/a.out
024f9000-0251a000 rw-p 00000000 00:00 0                                  [heap]
7fc0d8000000-7fc0d8021000 rw-p 00000000 00:00 0 
7fc0d8021000-7fc0dc000000 ---p 00000000 00:00 0 
7fc0de3cf000-7fc0de3e5000 r-xp 00000000 08:02 7343550                    /usr/lib/libgcc_s.so.1
7fc0de3e5000-7fc0de5e4000 ---p 00016000 08:02 7343550                    /usr/lib/libgcc_s.so.1
7fc0de5e4000-7fc0de5e5000 rw-p 00015000 08:02 7343550                    /usr/lib/libgcc_s.so.1
7fc0de5e5000-7fc0de77d000 r-xp 00000000 08:02 7343235                    /usr/lib/libc-2.23.so
7fc0de77d000-7fc0de97c000 ---p 00198000 08:02 7343235                    /usr/lib/libc-2.23.so
7fc0de97c000-7fc0de980000 r--p 00197000 08:02 7343235                    /usr/lib/libc-2.23.so
7fc0de980000-7fc0de982000 rw-p 0019b000 08:02 7343235                    /usr/lib/libc-2.23.so
7fc0de982000-7fc0de986000 rw-p 00000000 00:00 0 
7fc0de986000-7fc0de9a9000 r-xp 00000000 08:02 7343234                    /usr/lib/ld-2.23.so
7fc0deb82000-7fc0deb85000 rw-p 00000000 00:00 0 
7fc0deba8000-7fc0deba9000 rw-p 00000000 00:00 0 
7fc0deba9000-7fc0debaa000 r--p 00023000 08:02 7343234                    /usr/lib/ld-2.23.so
7fc0debaa000-7fc0debab000 rw-p 00024000 08:02 7343234                    /usr/lib/ld-2.23.so
7fc0debab000-7fc0debac000 rw-p 00000000 00:00 0 
7fff8ace4000-7fff8ad05000 rw-p 00000000 00:00 0                          [stack]
7fff8ad75000-7fff8ad77000 r--p 00000000 00:00 0                          [vvar]
7fff8ad77000-7fff8ad79000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
[1]    13833 abort (core dumped)  ./a.out

References and
Borrowing

Borrowing

fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
    // do stuff with v1 and v2

    // hand back ownership, and the result of our function
    (v1, v2, 42)
}

let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];

let (v1, v2, answer) = foo(v1, v2);
fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
    // do stuff with v1 and v2

    42
}

let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];

let answer = foo(&v1, &v2);

Rules for borrowing/references

1. 

References

Rust

By ..

Rust

  • 1,894