Skip to content

Latest commit

 

History

History
438 lines (329 loc) · 15.3 KB

meet-rust.md

File metadata and controls

438 lines (329 loc) · 15.3 KB

Meet Rust

Rust is a general purpose programming language with an emphasis on expressiveness and safety.

Rust is a multi-paradigm programming langage that emphasys imperative programming, object oriented programming and functionnal programming styles.

Rust is a statically typed language : the compiler checks that every possible path of execution will use values only in ways consistent with their types. The compiler can usually infer what type we want to use based on the value and how we use it. Rust's type system is inflenced by ML (Meta Language), the first version of Rust was written in OCaml before it was rewritten in Rust in 2011.

Rust was create by Mozilla to write servo. It have a growing fast community and a swaggy mascot : Ferris


                   _~^~^~_
               \) /  o o  \ (/
                 '_   u   _'
                 \ '-----' /

Rust features in "short" :

  • Memory Safe
  • Compiler that block lot of runtime errors
  • Interface with C/C++
  • Generic
  • Polymorphism
  • No garabage collector
  • No manual memory allocation / desallocation
  • No segmentation fault
  • No data race
  • Amazing toolchain
  • Compile to native apps, libs, webassembly or even node native addons.

First tour

Open the try site and try each piece of code bellow, step by step

Rust code uses snake case as the conventional style for function and variable names. In snake case, all letters are lowercase and underscores separate words.

Binding

Variable declaration and assignement use let binding :

fn main() {
    let greeting: &str = "hello";
    // prefix variable with _ avoid warnings from compiler for unused variable
    let _greeting = "hello"; //Rust do inference
    let score: i16 = 10;

        println!("{} with {} kisses",greeting, score)
}

Bidings are immutables by default, but you can make them mutable by adding mut in front of the variable name. In addition to allowing this value to change, mut conveys intent to future readers of the code by indicating that other parts of the code will be changing this variable value.

fn main() {
    let mut score = 10;
    println!("{} points", score);
    score = 11;
    println!("{} points", score)
}

However, you may create a new binding of the same name which shadows the previous binding :

fn main() {
    let greeting = "hello";
    println!("{}", greeting);
    let greeting = "bye";
    println!("{}", greeting)
}

Ownership

When you assign a variable to another some language copy the value (like C++), some other copy the reference (like python). Rust use a different mechanism. Rust’s central feature to manage memory is ownership. Although the feature is straightforward to explain, it has deep implications for the rest of the language.

First, let’s take a look at the ownership rules. Keep these rules in mind :

  • Each value in Rust has a variable that’s called its owner.
  • There can only be one owner at a time. So when you use a variable you move ownership.
  • When the owner goes out of scope, the value will be dropped.

Assigment move ownership of a variable :

fn main() {
    let greeting = String::from("hello"); // in fact "hello" was a string slice
    let greeting2 = greeting; // ownership move to greeting2
    println!("{}", greeting) // error greeting does not exist any more
}

Pass a function's parameter move ownership of a variable :

fn main() {
   let greeting = String::from("hello");
   println!("{}", add_world(greeting)); // ownership move to addWorld function
   println!("{}", greeting) // error greeting does not exist any more
}

fn add_world (s: String) -> String {
    s + &(String::from(" world")) // last expression is an implicit return. No semicolon !!!
}

fn useless_add_world (s: String) -> () {
    s + &(String::from(" world")); // Semicolon ends an expression. In this case the last expression is implicitily () that have unit type (similar to void in C/C++)
}

To reuse variable, you can clone them :

fn main() {
    let greeting = String::from("hello"); // in fact "hello" was a string slice
    let greeting2 = greeting.clone(); // make a copy of greeting
    println!("{}", greeting); // error greeting does not exist any more
}

You can also borrow a reference to the variable :

fn main() {
   let greeting = String::from("hello");
   let greeting2 = &greeting; // &greeting  lets us create a reference that refers to the value of greeting but does not own it
   println!("{}", greeting2);
   println!("{}", greeting); // works !
}
fn main() {
   let greeting = String::from("hello");
   let greeting2 = add_world(&greeting); // &greeting  lets us create a reference that refers to the value of greeting but does not own it
   println!("{}", greeting2);
   println!("{}", greeting); // works !
}

fn add_world (s: &String) -> String {
    let s_world = s.clone() + &(String::from(" world")); // +infix operator (+) concat String with &str (string slice)
    s_world
}

This introduce some restrictions like a borrowed content cannot be moved :

fn main() {
   let greeting = String::from("hello");
   let greeting2 = add_all(&greeting);
}

fn add_all (s: &String) -> String {
    let s_world = *s + &(String::from(" all")); // compilation error
    s_world
}

You can notice that & give a reference to a value, the opposite is dereference operator is accomplished by *

There is an exception to the move of ownership mechanism, the type of the variable implement the Copy trait. This is the case by default for primitive like bool, integer types (i16, u32, ...), floating types (f64, ...), char, ... and tuples which only contain types with Copy trait : (i32, u16) does Copy, but (i32, String) doesn't Copy

fn main() {
    let score = 10;
    let score2 = score; // score2 copy score
    println!("{}", score); // print 10
}

By having this model, Rust prevents several errors, like seg faults. It is similar to Read-Writers lock :

  • Many readers at once OR a single writer with exclusive access
  • Read only do not require exclusive access
  • Exclusive access do not allow other readers

If it is a new concept for you, you should read this

Types

The type system is completely "sound" by default. This means that, as long as your code compiles fine in safe mode, every type guarantees that it's not lying about itself. In a conventional, best-effort type system, just because the type says it's e.g. "an integer that's never null", doesn't mean it's actually never null. In contrast, a pure Rust program has no null bugs.

This code gives a copilation time error :

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.

Rust types can be inferred. The type system deduces the types for you even if you don't manually write them down.

let greeting = "hello";
let score = 10;

Alias types can refer to a type by a different name. They'll be equivalent:

type Score = i16;
let s1 : Score = 10;

Rust provides two list primitives : Array and Vector. Arrays are useful when you want your data allocated on the stack rather than the heap. Arrays are homogeneous, immutable, fixed length.

    let heroes_array : [&str; 3] = ["Carl", "Rick", "Michonne"];
    let first = heroes_array[0]; // accessing first element
    let second = heroes_array[1];

A vector is a similar collection type provided by the standard library that is allowed to grow or shrink in size.

    let heroes_vector : Vec<String> = vec![String::from("Carl"),String::from( "Rick"), String::from("Michonne")];
    let first = &heroes_vector[0];
    assert_eq!(first, "Carl");
    let middle = &heroes_vector[1..3];
    assert_eq!(middle, [ "Rick", "Michonne"]);
    let copy_heroes = heroes_vector.clone(); // clone heroes_vector which cannot be move because previously borrowed
    let mut more_heroes = vec![String::from("Negan")];
    more_heroes.extend(copy_heroes);// copy_heroes is moved and can't be used anymore
    assert_eq!(more_heroes, [ "Negan", "Carl", "Rick", "Michonne"]);

####Product types

/* Tuples are immutable, ordered, fix-sized at creation time heterogeneous */
let name_heart: (&str, i32) = ("Negan", 10);
type coord3d = (i32, i32, i32);
let warehouseCoord: coord3d = (1, 4, 18);

/* Records are immutable by default and fixed in field names and types */
struct Hero {
  heart: i32,
  name: String,

}
let negan = Hero { heart : 10, name : String::from("Negan")};
let name = String::from("Carl");
let carl = Hero {
    name, // punning
    heart: 10,
};

####Variant types

enum LifeVariant {
    Alive(Hero),
    Dead,
    Zombi
}

let how_is_Negan = LifeVariant::Alive(negan);
let how_is_Lori = LifeVariant::Dead;

Alive, Dead and Zombi are called "constructors" (or "tag"). A variant's constructors need to be capitalized. Type constructor may have parameters

Algebraic data types come with one of the most important features : pattern matching

fn get_message(how_are_you: LifeVariant) -> String{
     match how_are_you {
      LifeVariant::Zombi => String::from("Aaaaaarg !"),
      LifeVariant::Dead => String::from("!!!"),
      LifeVariant::Alive(h) => String::from("Great! ") + &h.name + " is alive",
    }

};
assert_eq!(get_message(how_is_Negan), String::from("Great! Negan is alive"));
assert_eq!(get_message(how_is_Lori), String::from("!!!"));

For some use cases, when matching enums, match is awkward. if let is cleaner for some use case and in addition allows various failure options to be specified:

let how_is_zombi = LifeVariant::Zombi;

fn get_message2(how_are_you: LifeVariant) -> String{
    // The `if let` construct reads: "if `let` destructures `how_are_you` into
    // `LifeVariant::Alive(h)`, evaluate the block (`{}`).
    if let LifeVariant::Alive(h) = how_are_you  {
         String::from("Great! ") + &h.name + " is alive"
    } else{
        String::from("No more hope")
    }
};
assert_eq!(get_message2(how_is_zombi), String::from("No more hope"));

Options

Rust itself doesn't permit the notion of null or undefined in safe code. This is a great thing, as it wipes out an entire category of bugs. No more undefined is not a function, and cannot access foo of undefined!

We represent the existence and nonexistence of a value by wrapping it with the option type. Here's its definition from the standard library:

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

It means "a value of type option is either None (nothing) or that actual value wrapped in a Some". It's easy to unwrap values from option type using pattern matching.

type Weapon =  Option<String>;

fn is_dangerous(w: Weapon) -> String {
  match w {
   None  => String::from("not dangerous"),
   Some(a) => String::from("is armed with a ") + &a,

  }
};

let unarmed: Weapon = None;
let knife: Weapon = Some(String::from("knife"));
assert_eq!(is_dangerous(unarmed), String::from("not dangerous"));
assert_eq!(is_dangerous(knife), String::from("is armed with a knife"));

There is a shorter way to unwrap a value from an Option in Rust :

type Weapon =  Option<String>;

let unarmed: Weapon = None;
let knife: Weapon = Some(String::from("knife"));

assert_eq!(knife.unwrap(), String::from("not dangerous"));//unwrap panic if None
assert_eq!(knife.unwrap_or("not dangerous"), String::from("knife"));/unwrap_or provide a default value
assert_eq!(unarmed.unwrap_or("not dangerous"), String::from("not dangerous"));/

Control flow

In Rust if is an expression, we can use it on the right side of a let binding or a function's return

fn main() {
    println!("{}", get_person_label(10));
}

fn get_person_label(age: i32) -> String {
    if age < 10 {
        String::from("child")
    }else if age < 18 {
        String::from("teenage")
    }else{
       String::from("adult")
    }
}

Functions

Functions are declared with start with fn and have a set of parentheses after the function name. The curly brackets tell the compiler where the function body begins and ends. Rust also have lambdas.

 fn add (x:i32, y:i32) -> i32 {
      x+y
 }
 let add_lambda = |x: i32, y: i32| -> i32 {x + y}; // this is lambda syntax
 let add1 = |x| add(1, x); // You may use lambda to do partial application
 assert_eq!(add_lambda(1,2),add(1,2));
 assert_eq!(add1(2),add(1,2));

Rust's function are first class order : functions can be pass as parameters or return from another function.

 fn add (x:i32, y:i32) -> i32 {
      x+y
 }

// High Order Function
 fn calculate<A>( operation: A, x:i32, y:i32)  -> i32
                    where A: Fn(i32, i32) -> i32 {
    operation(x, y)
}

 assert_eq!(add(1, 2),calculate(add,1,2)); // with named function as parameter
 assert_eq!(add(1, 2),calculate(|x: i32, y: i32| -> i32 {x + y},1,2)); // with lambda as parameter

The above code shows a higher order function. The interesting piece in code is the type of firts parameter operation. operation is of generic type A which is defined in the where clause.
where clause in rust is used for type bound on generics. The type bound in our example says, A is of type Fn, one of the trait for function type, which takes two i32 values and returns i32.

As we can pass a function as parameter to a function, we can return a function from another function. This is quite simple in most of the functional programming languages, but it’s its little bit tricky in rust.

 fn add (x:i32, y:i32) -> i32 {
      x+y
 }

// High Order Function
fn gen_add<'a>(x:& 'a i32) ->
                            Box<Fn(i32) -> i32 + 'a > {
       Box::new(move |y:i32| y+x)
}
let add1 = gen_add(&1);

assert_eq!(add(1, 2), add1(2));

The above code looks complicated. The reason to have the complications is the way rust works with lifetimes. Let’s try to understand what’s going on here.

In our code, we are defining a function which take i32 value as parameter. Then the function should return a function which wraps this value with logic to increment given value with the parameter. The question we ask ourselves is how long this function lives?

In garbage collected language like Javascript, it’s easy as garbage collector take care of this issue. But rust doesn’t have a gc. So rust has to determine the lifetime of the function in the compile time only.

In our example, we are defining a scope ‘a which is associate a scope with input value. So we are saying here to compiler, keep lifetime of function as long as value x exist. Lifetimes in rust can only exist with references. So in our example we will take &i32 rather than i32. Also we create to reference to Fn using Box.

Now we understand the lifetime of formal parameters and return type. But what about move in our implementation? The value we take as parameter is created in stack. So when function returns the x is destroyed. So move says move the ownership of x as part of closure.