learn Rust lang

May 12, 2020

Rust is a system programming language. Used in building web browsers, web servers, databases, firmware, tools and utilities, gaming and operating systems. No garbage collection. Memory safety. Use after free, Dangling pointers, Null pointer exceptions, Iterator invalidation, data races. Zero cost Abstractions. Integrated build tooling Package Manager Open-source package repository Default testing framework Autogenerated documentation. Community- Rust users forum, Internals forum, discord, RustConf, This week in Rust newsletter, rust-lang on twitter and reddit.

update - rustup update

Hello, world main.rs

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

Compile rustc main.rs

Run ./main.

cargo hello world cargo new hello-world cargo build cargo run

Variables

Variables are bound to values using the keyword argument let. Immutable by default.This creates predictability in our code. Can be mutable by adding mut keyword.

Constants are declared with const keyword. Names of these constants are capitalized with underscore. can only be set to expressions.

Shadow a variable. Declare a new variable with the same name as the previous, creating a new binding.The new variable will shadow the old variable.

fn main() {
    let mut x = 1;
    x = 3;
    println!("The value of x is : {}", x);
    let y = true;
    let y = false;
    println!("The value of y is : {}", y);

    const STRING: &str = "hello";
    println!("The value of constant string is : {}", STRING);
}

Data Types

Scaler types

  1. Integers

    • Integers are whole numbers.
    • They’re either signed or unsigned(+ve or -ve).
    • compiler must know the data types for each variable in your code.
    • The data type can be inferred by the compiler.It defaults to i32.
    • isize, usize(8 bit, i8, u8)
  2. Floating point

    • Numbers with decimal points.
    • types: f32, f64
    • default: f64.
  3. Booleans

    • have a value of either ‘true’ or ‘false’.
    • specified using keyword bool.
    • 1 byte in size.
    • Used most in conditional and control flow statements.
  4. Characters

    • Represents letters.
    • specified using keyword char.
    • Use single quotes.
fn main() {
    let a = 5; //i32
    let b: u8 = 1;// u8

    let x = 2.0; //f64
    let y: f32 = 3.0; //f32

    let sum = a + b;
    
    let difference = x - 1.0;

    let product = 4 * a;

    let quotient = 9.0 / y;
    let remainder = a % b;

    let t = true;
    let f = false;

    let d = 'D';
}

Compound types

  1. Arrays

    • Continuous group of items.
    • Fixed length.
    • Length known at compile time.
    • heterogeneous(contain items of the same data type)
    • Access items by index.
    • compiler warns when index is known to be out of bounds.
    • Panics at runtime when index is out of bounds.
  2. Tuples

    • Continuous group of items.
    • fixed length.
    • Length known at compile time.
    • Homogeneous(contain items of the different data types)
    • Empty tuple are called unit.
    • Access items by index using dot notation.
    • Impossible to access items out of bounds.
fn main() {
    let array = [1u32, 3, 4];
    let first_element = array[0];
    let len = "some text".len();

    let tuple = (1u32, 2,"do",  true);
    let first_element = tuple.0;
}
  1. Functions

    • Start with fn.
    • The function name.
    • An optional list of arguments. Stating arguments type is required.
    • An optional return type. A return type is required if a value is returned. If not return type is unit.
    • The function body.
fn main() {
    println!("Hello, world!");
    last_char(String::from("Hello"));
}

fn last_char(string: String) -> char {
    if string.is_empty() {
        return 'O';
    }
    string.chars().next_back().unwrap()
}
  1. Structs

    • A type composed of other types
    • Can contain different types.
    • Three flavours of structs

      • Classic - Most commonly used, each field has a name and a type.
      • Tuple - Similar to classic structs, have no names.
      • Unit - have no fields, similar to () unit type.
    • Use keyword struct followed by the name of the struct.
    • Name describe the object appropriately.
    • Create an instance of the struct by supplying key: value pairs.
    • More instances can be created.
    • access properties with the dot notation.
struct Person {
    name: String,
    age: u8,
    likes_oranges: bool
}

struct Point2D(u32, u32);
fn main() {
   let person = Person{
       name: String::from("Davis"),
       likes_oranges: false,
       age: 20,
   };

   println!("Person name is: {}", person.name);

   let origin = Point2D(100, 200);
   println!(" Point contains {:?} and {:?}", origin.0, origin.1);

   //destructuring
   let Point2D(x, y) = origin;
   println!(" Point contains {:?} and {:?}", x, y)
}

Structs from rust book

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

//Associative functions
impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}


fn main() {
    let mut user = User{
        email: String::from("john@example.com"),
        username: String::from("john"),
        active: true,
        sign_in_count:1
    };

    let name = user.username;
    user.username = String::from("john_doe");

    let user2 = build_user(String::from("janedoe@email.com"), String::from("jane"));

    let user3 = User {
        email: String::from("Jack@example.com"),
        username: String::from("jack"),
        ..user2
    };

    //tuple structs
    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);

    let rect = Rectangle {
        width: 30,
        height: 50
    };
    println!("rect: {:#?}", rect);
    println!("The area of the rectangle is: {}", area(&rect));
    //using implementation method
    println!("The area of the rectangle is: {}", rect.area());

    let rect2 = Rectangle {
        width: 20,
        height: 40
    };

    let rect3 = Rectangle {
        width:40,
        height: 50,
    };

    println!("rect can hold rect1: {}", rect.can_hold(&rect2));
    println!("rect can hold rect1: {}", rect.can_hold(&rect3));

    let rect4 = Rectangle::square(25);
    println!("{:#?}", rect4);
}

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count:1,
    }
}

fn area(dimensions: &Rectangle) -> u32 {
    dimensions.width * dimensions.height
}
  1. Enums

    • List all variations of some data.
    • Common across programming languages.
    • Referred to as algebraic data types.
    • use the keyword enum followed by the name.
    • List all variations.
    • The enum is now a custom data type that can be used in code.
    • Can include any kind of data.
    • can have a variety of types.
    • Similar to structs but with more flexibility and advantages.

      • Describe what kind of data will be stored.
      • Each variant can have a different type.
      • All variants stored under the custom enum type.
enum WebEvent {
    PageLoad,
    PageUnload,
    KeyPress(char),
    Paste(String),
    Click{x:i64, y:i64},
}

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

fn main() {
    let quit = WebEvent::KeyPress('q');

    let something = Some(1);
}

Enums from rust book

enum IPAddrKind {
    v4,
    v4u(u8, u8, u8, u8),
    v6(String),
}

struct IPAddr {
    kind: IPAddrKind,
    address: String,
}

enum Message {
    Quit,
    Move { x: i32, y: i32},
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message {
    fn some_function() {
        println!("enum implements Message");
    }
}

fn main() {
    let four = IPAddrKind::v4;
    let six = IPAddrKind::v6;

    let localhost = IPAddr {
        kind: IPAddrKind::v4,
        address: String::from("127.0.0.1"),
    };

    let localhost2 = IPAddrKind::v4u(127, 0, 0, 1);

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

    let some_number = Some(1);
    let some_string = Some("A string");
    let absent_number: Option<i32> = None;

    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y.unwrap_or(0);
    println!("{}", sum);

    value_in_cents(Coin::Quarter(UsState::Alaska));

    let ten = Some(10);
    let eleven = plus_one(ten);
    let none = plus_one(None);

    let some_value = Some(3);
    match some_value {
        Some(3) => println!("Three"),
        _ => (),
    }
    // better approach
    if let Some(3) = some_value {
        println!("Three");
    }
    
}

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    Arizona,
    Arkansas,
    California,
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State Quarter from {:?}", state);
            25
        }
    }
}

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
        // _ => None,
    }
}

Control flow

if … else, else if expressions

Provide a condition , and then execute a block of code if the condition evaluates to true. An else expression can be added optionally. If no else is provided, the program will skip the if block if the condition evaluates to false. If you have more than two conditions to check, if and else can be combined in an else if expression.

In the case all if and else if conditions evaluates to false, then the else block is executed.

match

Similar to switch in other programming languages. A scrutiny expression is provided to compare to the patterns. Arms are evaluated and compared with the scrutiny expression.

The scrutiny expression is x. Each arm has a pattern and some code. The ”=>” operator separates the pattern and the code to run. The first arm with the matching pattern is executed.

fn main() {
    if 1 == 2 {
        println!("match is broken");
    } else {
        println!("Everything is fine");
    }

    let formal = true;
    let greeting = if formal {
        println!("Good evening");
    } else {
        println!("Hey, friend!");
    };


    let number = 6;

    if number % 4 == 0 {
        println!("Number is divisible by 4");
    } else if number % 3 == 0 {
        println!("Number is divisible by 3")
    } else {
        println!("Number is not divisible by 3 or 4");
    }

    let boolean = true;

    let binary = match boolean {
        false => 0,
        true => 1,
    };

}

Loop

Used to execute over a block of code forever. Or until it is stopped, or the program quits. Instead of having this code run infinitely, the break keyword can be used.

while loop

Conditional loops Run until condition is met or become false.

for loop

Iterate through elements in a collection. Each pass of the loop extracts a values.

fn main() {
    let mut i = 1;
    let something = loop {
        i *= 2;
        if i > 100 {
            break i;
        }
    };
    assert_eq!(something, 128);

    let mut counter = 0;

    while counter < 10 {
        println!("Hello");
        counter += 1; 
    }

    for item in 0..5{
        println!("{}", item*2);
    }
}

Error handling

Simplest method is using the panic!() macro.

What happens when panic is encountered is:

  • Failure message is printed.
  • Program unwinds and cleans up the stack. A panic should be used only when a program comes to unrecoverable state. Rust emits a panic during code execution.
use std::fs::File;
fn main() {

    // let v = vec![0, 1, 2, 3 ];
    // println!("{}", v[6]);
    // panic!("I Panicked!");
    let fruits = vec!["banana", "apple", "orange", "coconuts"];
    let first =  fruits.get(0);
    println!("{:?}", first);
    let third = fruits.get(2);
    println!("{:?}", third);
    let non_existent = fruits.get(399);
    println!("{:?}", non_existent);

    let f = File::open("hello.txt").unwrap(); //or use .expect("Failed to open hello.txt"); or
    // let f = match f {
    //     Ok(file) => file,
    //     Err(error) => panic!("Can't open file {:?}", error)
    // };
}

Ownership

About how your program manages memory. Rust stores data in 2 different structured parts of memory.

  • Stack - Last In, First Out(LIFO).Stores data with a known fixed size at compile time.
  • Heap

A vector is mutable hence it’s size can change when the program is running. Vector object stored on stack with memory pointer to heap. Value of vector is stored in the heap.

Rules of ownership

  1. Each value in Rust has a variable that is called its owner.
  2. There can only be one owner at a time. Rust invalidates the owner.
  3. When the owner goes out of scope, the value will be dropped.

Variable scope - Range within a program for which that variable and the value are valid.

When we pass a variable as a parameter into a function, we transfer the ownership to that function.

throws an error

fn main() {
    let say = String::from("Cat");
    print_out(say);
    println!("Again! {}, ", say);
}

fn print_out(to_print: String) {
    println!("{}", to_print);
}

Works

fn main() {
    let say = String::from("Cat");
    print_out(say.clone());
    println!("Again! {}, ", say);
}

fn print_out(to_print: String) {
    println!("{}", to_print);
}

Ownership prevents memory safety issues

  • Dangling pointer
  • Double-free - trying to free memory that has already been freed.
  • Memory leaks - not freeing memory that should have been freed.
Ownership from rust book

You can have multiple immutable reference but not multiple immutable references.

fn main() {
    // Ownership rules
    //1. Each variable in rust has a value called it's owner
    //2. There can only be one owner at a time
    //3. When the owner goes out of scope, the value will be dropped.

    //Stacks and heaps
    fn a() {
        let x = "hello";
        let y = 32;
        b()
    }

    fn b() {
        let x = String::from("world");
    }

    {// s is not valid here, it's not yet declared
    let s= "Hello, world!"; //s is valid from this point on
    let s2= String::from("Hello, world!"); //s is valid from this point on
    // you can do things with s
    }// this scope is now over, and is no longer valid

    let x = 5;
    let y = x; //copy for integers, characters, booleans

    let string1 = String::from("Hello");
    let string2 = string1; //A move i s performed not shallow copy
    let string3 = string2.clone(); //A clone

    let string4 = String::from("Takes Ownership");
    takes_ownership(string4);
    //println!("{}", string4); // returns an error, ownership lost to the function

    let my_int = 5;
    makes_copy(my_int);
    println!("{}", my_int); // prints the integer my_int

    let string5 = gives_ownership();
    println!("{}", string5); // prints the string

    let string6 = takes_and_gives_back(string5); // takes ownership and gives back
    println!("{}", string6);//prints the string

    let string_7 = String::from("Does'nt take ownership");
    let (string_8, len) = calculate_length(string_7);
    println!("The length of {} is {}", string_8, len);

    //or
    let string_7_2 = String::from("Does'nt take ownership");
    let len= calculate_length_2(&string_7_2);
    println!("The length of {} is {}", string_7_2, len);

    let mut string_9 = String::from("Hello");
    change_reference(&mut string_9);

}



fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}

fn makes_copy(some_integer:i32) {
    println!("{}", some_integer);
}

fn gives_ownership() -> String {
    let some_string = String::from("Hello, world!");
    some_string
}

fn takes_and_gives_back( a_string: String) -> String {
    a_string
}

fn calculate_length(s:String) -> ( String, usize) {
    let length = s.len();
    (s, length)
}

fn calculate_length_2(s: &String) -> usize {
let length = s.len();
length
}

fn change_reference(some_string: &mut String) {
    some_string.push_str(", world");
}

Borrowing

Creates a reference to a value.

Rules for borrowing

  1. At any given time, you can have either:

    • One mutable reference, or
    • Any number of immutable references.
  2. References to must always be be valid

    • A value is no longer valid after it is dropped.
    • Any reference to that value also become invalid when it is dropped.

Works

fn main() {
    let say = String::from("Cat");
    let say2 = &say;

    println!("{}", say);
    println!("{}", say2);
}

Errors

fn main() {
    let say = String::from("Cat");
    let say2 = &say;

    println!("{}", say);
    drop(say);
    println!("{}", say2);
}
fn main() {
    let say = String::from("Cat");
    print_out(&say);
    println!("Again! {}, ", say);
}

fn print_out(to_print: &String) {
    println!("{}", to_print);
}
fn main() {
    let mut my_vec = vec![1,2,3];
    println!("{:?}", my_vec);
    add_to_vec(&mut my_vec);
    println!("{:?}", my_vec);
}

fn add_to_vec(a_vec: &mut Vec<i32>) {
    a_vec.push(4);
}

Look into “string slices” &str.

Strings

String types in rust include: String and &str, CString and &CStr, OsString and &OsStr.

The String type

  • An owned string.
  • Owns string data.
  • Data freed when dropped.
fn main() {
    string_len(String::from("Hello, world!"));
}

fn string_len(s: String) -> usize {
    s.len()
}
// s is dropped here

String memory consist of 3 parts

  • Length
  • Capacity
  • Data pointer

The &str type

  • A borrowed string slice.
  • Does not own string data.
  • Data not freed when dropped.
  • View/window into string data.

&str memory contains 2 parts:

  • Length
  • Data pointer
fn main() {
    string_len("Hello, world!");
}

fn string_len(s: &str) -> usize {
    s.len()
}

** String literals** are embedded into the binary, and have a type &str.

In rust Characters:

  • Strings are not arrays of characters.
  • Not every byte is a full character.
  • Access data using explicit methods.
fn main() {
    let text = "Hello\nWorld\n!";
    let upper = text.to_uppercase();
    let stripped = upper.strip_prefix("HELLO\n").unwrap();
    println!("{}", first_line(stripped));
}

// pub fn first_line(string: String) -> String {
//     string.lines().next().unwrap().to_owned()
// }

pub fn first_line(string: &str) -> &str {
    string.lines().next().unwrap()
}

Collections

  1. Vec - Access elements by index.(continuos collection of items)

    • Consist of 3 parts( Length, capacity, data pointer) Slices - continuos chunks of memory, refer to data owned by another value. Written as &[T]. Has 2 parts , length and data pointer.

      let vec = vec![1u8, 2, 3];
      let s = &v[0..3];
  2. HashMap<K, V> - Associate keys with values.
  3. HashSet - Unique items and no access by index.
  4. VecDeque - A queue.
  5. LinkedList - Doubly linked list.
fn main() {
    let mut students = vec![Student{
        name: "John".to_string(),
    }];

    students.push(Student{
        name: "Jane".to_string(),
    });

    assert!(
        &students[0] == &Student{
            name: "John".to_string()
        }
    );

    assert!(
        students.get(0) == Some(&Student{ name: "John".to_string()})
    );

    assert!(
        students.get(1000) == None
    );

    for student in students.iter() {
        println!("Student name: {}", student.name);
    }

    use std::collections::HashMap;

    let mut enrollment = HashMap::new();
    enrollment.insert("biology".to_string(), students);

    let bio_students = enrollment.get("biology");
    let students = enrollment.remove("biology");

}

#[derive(PartialEq, Eq,)]
struct Student {
    name: String,
}

Traits

In rust data(enums/structs) and behavior(traits) are stored separately. Can define default behavior for a method

pub struct  Person {
    name: String
}

pub struct  Cat {
    name: String
}

pub struct Rabbit {
    name: String
}

pub trait  Eat {
    fn eat_dinner(&self) {
        println!("I eat from a dish");
    }
}

impl Eat for Person {
    fn eat_dinner(&self) {
        println!("I eat from a plate");
    }
}

impl Eat for Cat {
    fn eat_dinner(&self) {
        println!("I eat from a cat bowl");
    }
}

impl Eat for Rabbit {
    
}

fn main() {
    let person = Person {
        name: String::from("Davis")
    };

    person.eat_dinner();

    let cat = Cat {
         name: String::from("Zane")
        };
    cat.eat_dinner();

    let rabbit = Rabbit {
        name: String::from("Rabbit")
    };

    rabbit.eat_dinner();
}
struct Film {
    title: String,
    director: String,
    studio: String,
}

struct Book {
    title: String,
    author: String,
    publisher: String,
}

trait Catalog {
    fn describe(&self){
        println!("We need more info about this type of media");
    }
}

impl Catalog for Film {
    fn describe(&self) {
        println!("{} was directed by {} through {} studios", self.title, self.director, self.studio);
    }
}

impl Catalog for Book {
    fn describe(&self) {
        println!("{} was written by {} and published by {} .", self.title, self.author, self.publisher);
    }
}

struct Album {
    title: String,
    artist: String,
    label: String
}

impl Catalog for Album {

}
fn main() {
    let capt_marvel = Film {
        title: String::from("Captain Marvel"),
        director: String::from("Ann Boden and Ryan Fleck"),
        studio: String::from("Marvel")
    };

    capt_marvel.describe();

    let elantris = Book {
        title: String::from("Elantris"),
        author: String::from("Brandon Sanderson"),
        publisher: String::from("Tor Books")
    };

    elantris.describe();

    let let_it_be = Album {
        title: String::from("Let it be"),
        artist: String::from("Beatles"),
        label: String::from("Apple")
    };

    let_it_be.describe();
}

Build a guessing game

Install a dependencies by adding it to cargo.toml and run cargo build

Build a rust Guessing game

Add rand to cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.5.5"
colored = "2.0.0"
use std::io;
use rand::Rng;
use  std::cmp::Ordering;
use colored::*;
fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess");

        let mut guess = String::new();
    
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
    
            let guess: u32 = match guess.trim().parse() {
                Ok(num) => num,
                Err(_) => continue,
            };
    
        println!("You guessed: {}", guess);
    
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("{}","Too Small!".red()),
            Ordering::Greater => println!("{}","Too Big!".red()),
            Ordering::Equal => {
                println!("{}","You win!".green());
                break;
            } 
        }
    }
}

Profile picture

Written by Davis Bwake A fullstack developer who likes JavaScript and everything web 3.0(BlockChain..) follow me on twitter

My tech stack is HTML, CSS,JavaScript, ReactJS, NodeJS, Solidity

Am currently open to remote job oppurtunities.

Checkout my projects

YouTube