Disclaimer

This document serves as a supplementary reference for Rust programming, complementing the lectures presented in class. For official documentation, please refer to the Appendix section.

If you encounter any errors, kindly email me at chandr34 @ rowan.edu.

Chapter 1

Programming

Overview

Why Learn Programming?

“Everybody in this country should learn how to program a computer… because it teaches you how to think”
Steve Jobs

Problem Solving

Automation

How does coding help you personally?

Develop Interpersonal Skills

Coding is Creativity

Feeling empowered.

Why learn more than one Language?

Mastering more than one language is often a watershed in the career of a professional programmer. Once a programmer realizes that programming principles transcend the syntax of any specific language, the doors swing open to knowledge that truly makes a difference in quality and productivity. — Steve McConnell

Img Src: Brian Johnson (Quora.com)


description: Domain Specific Language

DSL

Domain-specific languages (DSLs) are super interesting because they're tailor-made for specific tasks or industries, like SQL for databases or HTML for web pages.

DSLs can reduce code complexity and increase productivity for specific tasks.

A Domain Specific Language is a programming language with a higher level of abstraction optimized for a specific class of problems. (a.k.a) Specialized to a particular application domain.

15+ DSL available

Some popular ones are

HTML - HyperText Markup Language

SQL - Structured Query Language

CSS - Cascading Style Sheets

MD - Markdown (github readme.md or https://stackedit.io/app#)

Mermaid - Mermaid (https://mermaid.live/)

Sed - Text Transformation

XML - eXtended Markup Language

UML - Unified Data Modeling

Terraform - Manage Cloud Resources

image src: https://tomassetti.me/domain-specific-languages/


description: General Programming Language

GPL

As the name suggests, Programming languages that are commonly used today

Python

JAVA

C++

RUST

and so on.

Learn and use it for developing a variety of applications.

Compiler vs Interpreter

Compiler

The compiler scans the entire source code and translates the whole of it into machine code at once.

The code you write is usually converted into another form that a computer knows how to run. This process is called compilation, and the period this happens is called "compile time."

After compilation, the program is launched, and the period it's running is called "runtime."

Types of programming languages that use compilers are

C

C++

Rust

Haskell

Erlang

Interpreters

The Interpreter translates just one statement of the source code at a time into machine code at runtime.

Type of programming languages that use interpreters are

Python

PHP

Perl

Ruby

What about JAVA?

The JAVA is a combination of both.

It first compiles the source code into byte code and uses Java Virtual Machine (JVM) to interpret the Java byte code.

Static vs Dynamic

Statically Typed

Statically typed languages check the types and look for type errors during compile time.

Static type means checking for types before running the program. This lets the compiler decide whether a given variable can perform the requested actions.

Explicit variable-type declarations are usually required.

It is easy to catch errors during development.

int a = "Hello";

The above statement will result in an error during compile time itself.

Popular languages that are statically typed are

C/C++/GO/Haskell/JAVA/SCALA/RUST

Dynamically Typed

Dynamically typed languages check the types and look for type errors during runtime.

Dynamic type means checking for types while running the program. It is risky when an application fails in production.

Explicit declarations are not required.

Variable assignments are dynamic and can be altered. (to some degree)

Popular languages that are dynamically typed are

Python/Ruby/Erlang/Javascript/PHP/Perl

Simple Python example

Strongly Typed vs Weakly Typed

Strongly typed languages don't allow implicit conversions between unrelated types.

You usually can't perform operations on incompatible types without explicit conversion in a strongly typed language. Python is strongly typed despite being dynamically typed.

For example, Python is a strongly-typed language

#Python

a = 21;            #type assigned as int at runtime.
a = a + "dot";   #type-error, string and int cannot be concatenated.
print(a);

Weakly typed languages make conversions between unrelated types implicitly.

Similarly, Javascript is a weakly-type language.

/*
As Javascript is a weakly-typed language, it allows implicit conversion
between unrelated types.
*/

a = 21;             
a = a + "dot";
console.log(a);

Programming Matrix

Can you spot Rust in this?

Image Source : Mayank Bhatnagar

Rust is both strongly typed and statically typed:

  • Strongly Typed: Without explicit conversion, you can't perform operations on incompatible types.
  • Statically Typed: The variable type is known at compile-time, not runtime.
let num = 5;
let text = "hello";

// The next line won't compile
// let result = num + text; // Error

Rust Overview

Rust Overview

"Rust deals with low-level details of memory management, data representation, and concurrency."

What is RUST?

  • A Mozilla employee, Graydon Hoare, started working on a language as a personal project.
  • Later, Mozilla sponsored the project in 2009 and announced it in 2010.
  • The first stable release was on May 15, 2015.

Rust is a system programming language that is safe and concurrent.

Many languages inspire Rust, such as

  • System Programming Languages (C, C++)
  • Functional Programming Languages (Haskell, Erlang)

Why Rust

Open Source: Larger community, nightly builds, adoption by big companies.

Reliability: Rust is reliable because its model allows you to eliminate a wide range of memory-related bugs at compile time.

Type Safe: The compiler assures that no operation will be applied to a variable of the wrong type.

Memory Safe: Rust pointers (references) always refer to valid memory.

Data Race Free: Rust's borrow checker guarantees thread safety by ensuring, that multiple parts of a program can't mutate the same value at the same time.

Speed: Rust is fast and memory-efficient. It can power production services, run on embedded devices, and easily interact with other languages because it has no runtime or garbage collector.

Targets Bare metal: Rust can target embedded and "bare metal" programming, making it suitable to write an OS kernel or device driver.

Security: Rust is one of the safest programming languages because it emphasizes memory safety by analyzing a program’s memory compilation at build time, preventing bugs and memory errors caused by poor memory management.

Efficiency: Rust’s efficiency stems from its ability to assist developers in writing performance code quickly due to its very minimal and optimal runtime.

Productivity: The Rust programming language boosts developer productivity by allowing them to create highly resilient applications.

Safety: Ensures memory safety without the need for garbage collection. It ensures memory safety using ownership and its borrowing concept.

Cargo Manager: It's the package manager for Rust. It's similar to npm, pip.

Error Messages: Powerful compiler with useful error messages. it not only displays the line which has the error but, also the type of error.

Efficient C Binding: The Rust language can interoperate with the C language. It provides a foreign function interface to communicate with C API’s. Due to its ownership rules, it can guarantee memory safety.

Stackoverflow 2022 Survey

https://survey.stackoverflow.co/2022/#technology-most-loved-dreaded-and-wanted

What Is Rust Used For?

Rust is fit for:

  • building powerful web applications
  • embedded systems programming
  • building distributed online services
  • cross-platform command line support

Who Uses Rust?

Some of the top companies listed here

  • Drop Box
  • Facebook
  • Microsoft
  • Discord
  • Cloudflare
  • Coursera
  • Firefox
  • Atlassian
  • Amazon's Firecracker
  • Databricks

Rust is self-hosted, meaning the Rust compiler is written in Rust.

The initial release was written using OCaml (General purpose programming language)

Rust is used for...

Src: Unknown

  • Audio programming: Rust's performance and low-level control are beneficial for developing audio processing applications.

  • Blockchain: Rust is used in blockchain projects like Substrate due to its performance and safety features.

  • Cloud computing applications: Rust can be used to build efficient cloud-native applications.

  • Cloud computing infrastructure or utilities: Rust's performance and concurrency are ideal for infrastructure tools and utilities.

  • Computer graphics: Libraries like gfx-rs are used for graphics programming in Rust.

  • Data science: While not as dominant as Python, Rust has growing support for data science through libraries like Polars.

  • Desktop computer application frontend: Frameworks like druid allow for building desktop applications.

  • Desktop computer or mobile phone libraries or services: Rust is used to create backend services and libraries that power applications.

  • Distributed systems: Rust is used in distributed systems projects like TiKV due to its performance and safety.

  • Embedded devices (bare metal): Rust's control over memory and safety features make it suitable for bare-metal programming.

  • Embedded devices (with operating systems): Rust can be used to develop applications for embedded devices with operating systems.

  • HPC (High-performance [Super]Computing): Rust is used in high-performance computing applications.

  • IoT (Internet of Things): Rust is suitable for IoT applications due to its low-level control and performance.

  • Machine learning: Rust's ecosystem is growing in machine learning, with libraries like tch-rs.

  • Mobile phone application frontend: Rust can be used in mobile app development, particularly for performance-critical components.

  • Computer networking: Rust is popular for building networking applications due to its safety and concurrency features.

  • Programming languages and related tools (including compilers, IDEs, standard libraries, etc.):

  • Rust is used for developing new programming languages and related tools.

  • Robotics: Rust's safety and performance are advantageous in robotics applications.

  • Computer security: Rust's safety features help in developing secure applications.

  • Scientific and/or numeric computing: Rust is used for scientific computing due to its performance.

  • Server-side or "backend" application: Rust is widely used for backend development.

  • Simulation: Rust is suitable for simulation applications due to its performance.

  • Web application frontend: Through WebAssembly, Rust can be used for web frontend development.

  • WebAssembly: Rust is frequently compiled to WebAssembly for high-performance web applications.

Commonly Used Attributes

In Rust, attributes provide metadata about the code to the compiler. Some commonly used attributes include:

#[derive(...)]: Automatically implements standard traits for a struct or enum.

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

#[cfg(...)]: Compiles code conditionally based on specified configuration options.

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

#[test]: Marks a function as a test function to be run by the test harness.

#[test]
fn test_addition() {
    assert_eq!(2 + 2, 4);
}

#[allow(...)] and #[deny(...)]: Controls compiler warnings and errors.

#[allow(dead_code)]
fn unused_function() {
    // This function is not used
}

#[deny(missing_docs)]
fn another_function() {
    // This will cause a compilation error if there are no docs
}

#[inline] and #[inline(always)]: Suggests to the compiler to inline the function.

#[inline]
fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[inline(always)]
fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

#[macro_use]: Imports macros from an external crate.

#[macro_use]
extern crate serde_derive;

#[derive(Serialize, Deserialize)]
struct MyStruct {
    field: String,
}

#[non_exhaustive]: Prevents exhaustive matching on enums, making it easier to add variants in the future.

#[non_exhaustive]
enum MyEnum {
    Variant1,
    Variant2,
}

Don't worry if these things don't make sense; we will learn all these things in the coming weeks.

Playground

If you just want to try a bit of Rust code or check the syntax for a definition in a Rust library. You might also be looking for a way to share some code with others.

The Rust language offers support for these tasks in the Rust playground.

https://play.rust-lang.org/

In the playground, you can access methods and functions in the Rust std standard library.

The top 100 most-downloaded crates in the crates.io library are also available along with their dependencies.


Demo

Let's try with a simple example.

fn main(){println!(Welcome to Rust!);}
Image Src: Microsoft Rust Documentation

The tool adjusts the code to follow official Rust styles:

Image Src: Microsoft Rust Documentation

Select Tools > Clippy to check for mistakes in the code. The results are displayed under the editor:

Image Src: Microsoft Rust Documentation

To fix the sample code, add quote marks around the text "Welcome to Rust!":

Image Src: Microsoft Rust Documentation

Now we'll compile the code and run the program.

Image Src: Microsoft Rust Documentation

Rust Installation

MAC

// install rustup 

// more details https://www.rust-lang.org/tools/install

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

// Verify the installation

# rustc --version

# cargo --version

Windows

Download the installer from this URL and execute the file.

https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-gnu/rustup-init.exe

Step 1: Type 1 and hit enter

Step 2: Setup will download and install Visual Studio Installer. It doesn't matter even if you have VS Code, as this is a different installer.

Step 3: Check the components and click Install.

Note: Based on the version of your windows, the screen will display the appropriate version number.

Step 4: Type 1 and press Enter to proceed with default installation. If you want to customize then type 2 and press Enter.

Step 5: If you had chosen option 2, then confirm your settings and type 1 to proceed with the installation.

Step 6: Goto Command prompt (Start > Run > cmd)

type

C:\> rustc --version

C:\> cargo --version

To verify the rust installation.

VS Code

Download and Install VS Code. (This is different from Visual Studio)

{% embed url="https://code.visualstudio.com/" %}

Windows Users

Create a new folder on your c:\

Let's call it

c:\learning\rustpgm

Open VS Code

Goto File > Add Folder to Workspace

Choose  c:\learning\rustpgm and click Add

Click Trust / Ok if it prompts.

Goto File > Save Workspace and save the workspace as **rustpgm.code-workspace**

Right-click on rustpgm and choose New File

Name the file as 01.rs

Copy the sample rust code

// Sample Code

fn main(){
    println!("Hello World");
}

Save the Script (press Ctrl + S or File > Save)

Click New Terminal

Make sure you are in this folder  c:\learning\rustpgm

type

**rustc 01.rs**

once compilation is done.

Verify  the result by listing the file contents

**dir**

Execute the program by typing the following

**./01.exe**

Mac Users

Create a new folder on your Documents folder

Let's call it

**/Documents/learning/rustpgm**  

**Open VS Code**

Goto File > Add Folder to Workspace

Choose  /Documents/learning/rustpgm and click Add

Click Trust / Ok if it prompts.

Goto File > Save Workspace and save the workspace as **rustpgm.code-workspace**

Right-click on rustpgm and choose New File

Name the file as 01.rs

Copy the sample rust code

fn main(){
    println!("Hello World");
}

Save the Script (press Ctrl + S or File > Save)

Click New Terminal

Make sure you are in this folder Documents/learning/rustpgm

type

rustc 01.rs

once compilation is done.

Verify the result by listing the file contents

ls

Execute the program by typing the following

./01

VS Code Extensions

Preferred Extensions for Rust

Cargo

Cargo frequently used subcommands

cargo --help

cargo --version

Creates project under new sub folder

cargo new projectname

Creates project under existing folder

cargo init projectname

Compile the current package

cargo build

Compile the current package for Production

* cargo build --release

Run the current package

cargo run

Check the current package for dependency errors

cargo check

Fetch dependencies of a package from the network

cargo fetch

Execute unit and integration tests of a package

cargo test

Remove generated artifacts

cargo clean

Update dependencies as recorded in the local lock file

cargo update

Build package's documentation

cargo doc

Format the code

cargo fmt

Refer more Cargo commands in Cargo Book

Cargo Book

Cargo Example

cargo new osinfo

cd osinfo

Replace the main.rs code with the code given below

// Rust sample for displaying OS Info

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

    let info = os_info::get();

    // Print full information:
    println!("OS information: {}", info);

    // Print information separately:
    println!("Type: {}", info.os_type());
    println!("Version: {}", info.version());
    println!("Bitness: {}", info.bitness());
}

Run the following statements

cargo check

cargo fetch

cargo build

check the executable under target/debug

cargo build --release

check the executable under target/release

The release executable will be smaller in size than debug executable.

Because, debug contains symbols & backtraces

Cargo dependency versions

While specifying the dependency version, you can either specify the exact one or mention the bottom range number.

Here are some more examples of version requirements and the versions that would be allowed with them:

You can mention the number given on the left-hand side instead for the range of versions given on the right-hand side.


1.2.3  :=  >=1.2.3, <2.0.0
1.2    :=  >=1.2.0, <2.0.0
1      :=  >=1.0.0, <2.0.0

0.2.3  :=  >=0.2.3, <0.3.0
0.2    :=  >=0.2.0, <0.3.0
0.0.3  :=  >=0.0.3, <0.0.4
0.0    :=  >=0.0.0, <0.1.0
0      :=  >=0.0.0, <1.0.0

//example using wildcards

*     := >=0.0.0
1.*   := >=1.0.0, <2.0.0
1.2.* := >=1.2.0, <1.3.0

// Comparison requirements

>= 1.2.0
> 1
< 2
= 1.2.3

// Multiple dependencies separated by comma

This means the version should be greater than or equal to 1.2 and less than 1.5

>= 1.2,  < 1.5

In the previous example (Cargo Example - 2), instead of mentioning

os-info = "3.5.0" we have mentioned

os-info = "3"

Using crate.io repositories

[dependencies]
regex = { git = "https://github.com/rust-lang/regex" }

Starter Crates

The Basic Program

Rust code is always put in a file with .rs extension

fn main() {
    println!("Hello World!");
}

Images Source: https://www.educative.io

Image Source: https://www.educative.io

Image Source: https://www.educative.io

Basic Formatting

Image Source: https://www.educative.io

In Rust, unlike other languages, we cannot directly print numbers or variables within the
macro. We need a placeholder trait{}.

In Rust, a trait is a way to define a set of methods that can be implemented on different types.

Positional Arguments

fn main() {
    println!("Number: {}", 1);
}
fn main() {
    println!("{} last name is {} ", "Rachel", "Green");
}

Named Arguments

fn main() {
    println!("{fname} last name is {lname} ", lname="Green", fname="Rachel");
}

Basic Math

fn main() {
    println!("{} * {} = {}",15, 15, 15 * 15);
}

A trait in Rust is a group of methods that are defined for a particular type.

Printing formatters

fn main() {
    println!("Number : 20 \nBinary:{:b} Hexadecimal:{:x} Octal:{:o}", 20, 20, 20);
}

Debug Trait {:?}

In Rust, the Debug trait is a built-in trait that allows types to be formatted for debugging purposes. It is primarily used by the println! and related macros to print a text representation of a value when used with the {:?} formatter.

fn main() {
    println!("{:?}", (100, "Rachel Green"));
}

Printing Styles

Source: http://www.educative.io

Comments

Line Comments //

Block Comments /* */

Doc Comments

Outer Doc Comments ///

Inner Doc Comments //!

// Writing a Rust program
fn main() {
    //The line comment is the recommended comment style
    println!("This is a line comment!"); // print hello World to the screen
}
/* Writing a Rust program */

/* comments can be /* nested */ too */

fn main() {
    println!("This is a line comment!");
}

Doc Comments are used to generate Documentation and they support markdown notations

/// This is a Doc comment outside the function
/// Generate docs for the following item.
/// This shows my code outside a module or a function
fn main() {
    //! This a doc comment that is inside the function   
    //! This comment shows my code inside a module or a function  
    //! Generate docs for the enclosing item
    println!("{} can support {} notation","Doc comment","markdown");
}

Variables

A variable is like a storage box paired with an associated name which contains data. The associated name is the identifier and the data that goes inside the variable is the value. They are immutable by default, meaning, you cannot reassign value to them.

Images Source: https://www.educative.io

To create a variable, use the let binding followed by the variable name

What is binding?

Rust refers to declarations as bindings as they bind a name at the time of creation. let is a kind of declaration statement.

Naming Convention: By convention, you would write a variable name in a snake_case i.e.,

  • All letters should be lower case.
  • All words should be separated using an underscore ( _ )

Initialize a variable

A variable can be initialized by assigning a value to it when it is declared. The value is said to be bound to that variable.

Note: It’s possible to declare the variable first and assign it a value later. However, it is not recommended to do this as it may lead to the use of uninitialized variables.

fn main() {
    let language = "Rust"; // define a variable
    println!("Language: {}", language); // print the variable
}

Note: it is not possible to directly print a variable within a println!(). You need a placeholder.

How to create a Mutable Variable

let mut variable = "value"
fn main() {
    let mut language = "Rust"; // define a mutable variable
    println!("Language: {}", language); // print the variable
    language = "Java"; // update the variable
    println!("Language: {}", language); // print the updated value of variable
}

Assigning Multiple Values

let (variable1,variable2) = ("value1", value2);
fn main() {
    let (fname,lname) =("Rachel","Green"); // assign multiple values
    println!(" Student Name is {} {}.", fname,lname); // print the value
}

If variables are unassigned or unused compiler will generate warning

#[allow(unused_variables, unused_mut)]
fn main() {
    let (fname,lname,mi) =("Rachel","Green",""); // assign multiple values
    println!(" Student Name is {} {}.", fname,lname); // print the value
}

Variable Scope

The scope of a variable refers to the visibility of a variable, or, which parts of a program can access that variable.

It all depends on where this variable is being declared. If it is declared inside any curly braces {}, i.e., a block of code, its scope is restricted within the braces, otherwise the scope is global.

Local Variable

A variable that is within a block of code, { }, that cannot be accessed outside that block is a local variable. After the closing curly brace, } , the variable is freed and memory for the variable is deallocated.

Global Variable

The variables that are declared outside any block of code and can be accessed within any subsequent blocks are known as global variables.

Images Source: https://www.educative.io

fn main() {
  let outer_variable = 112;
  { // start of code block
        let inner_variable = 213;
        println!("block variable inner: {}", inner_variable);
        println!("block variable outer: {}", outer_variable);
  } // end of code block
    println!("inner variable: {}", inner_variable); // use of inner_variable outside scope
}

How to fix this error?

fn main() {
  let outer_variable = 112;
  let inner_variable = 213;
  { // start of code block
        println!("block variable inner: {}", inner_variable);
        println!("block variable outer: {}", outer_variable);
  } // end of code block
    println!("inner variable: {}", inner_variable);
  }

Shadowing

Variable shadowing is a technique in which a variable declared within a certain scope has the same name as a variable declared in an outer scope. This is also known as masking. This outer variable is shadowed by the inner variable, while the inner variable is said to mask the outer variable.

Images Source: https://www.educative.io

fn main() {
  let outer_variable = 112;
  { // start of code block
        let inner_variable = 213;
        println!("block variable: {}", inner_variable);
        let outer_variable = 117;
        println!("block variable outer: {}", outer_variable);
  } // end of code block
    println!("outer variable: {}", outer_variable);
  }

Another - Variable reused in same scope

// Shadowing

fn main() {
   let spaces = "Testing";
   println!("{:?}",spaces);
   let spaces = spaces.len();
   println!("{:?}",spaces);
}

Simple Rust Programs

Using Variables

fn main() {
    let name = "Rachel";
    let age=30;
    println!("Hello {},{}", name,age);

    //change the value of variable
    
    let name = "Rachel Green";
    println!("Hello {},{}", name,age);

}

Using Multiple Variables

#[allow(unused_variables, unused_mut)]

fn main() {
    let (fname,lname,mi) =("Rachel","Green",""); // assign multiple values
    println!(" Student Name is {} {}.", fname,lname); // print the value
}

FOR Loops

// non inclusive on right side

fn main() {
    for i in 0..5 {
        println!("Hello {}", i);
    }
}

ODD / EVEN

fn main() {
    for i in 0..5 {
        if i % 2 == 0 {
            println!("even {}", i);
        } else {
            println!("odd {}", i);
        }
    }
}

Alternate Method

// Expression assigned as Value

fn main() {
    for i in 0..5 {
        let even_odd = if i % 2 == 0 {"even"} else {"odd"};
        println!("{} {}", even_odd, i);
    }
}

Pretty Please - Print

// Print

fn main() {
    let doesnt_print = ();
    println!("This will not print: {}", doesnt_print); // ⚠️
}

Pretty Print

// Pretty Print

fn main() {
    let doesnt_print = ();
    println!("This will print: {:#?}", doesnt_print); // ⚠️
}

What is the output of this?

// Print Space

fn main() {
    let doesnt_print = ' ';
    println!("This will not print: {}", doesnt_print); // ⚠️
}
// Pretty Print Space

fn main() {
    let doesnt_print = ' ';
    println!("This will print: {:#?}", doesnt_print); // ⚠️
}

Escape Printing

Similar to other programming languages \t and \n are used for Tab and Newlines

// \t \n

fn main() {
    print!("\t first line is tabbed \nand second line is on a new line");
}

How to print \t and \n?

// Escape Characters

fn main() {
    println!("Here are two escape characters: \\n and \\t");
}
// Print multiple \\ , "

fn main() {
    println!("File \"folder location is at c:\\users\\ganesh\\Documents\\01.rs.\" ") 
}

Too many \\ there is a good chance one might forget to \\, is there an easy way?

// r#  and  #  

fn main() {
    println!(r#"File "folder location is at c:\users\ganesh\Documents\01.rs." "#) 
}

If you need to print #, then use ##

// # & ##

fn main() {
    let hashtag_string = r##"The hashtag #IceToSeeYou had become very popular."##; // Has one # so we need at least ##
    let many_hashtags = r####""You don't have to type ### to use a hashtag. You can just use #.""####; // Has three ### so we need at least ####

    println!("{}\n{}\n", hashtag_string, many_hashtags);
}

Alternate Use

Not a good programming practice to use keywords as variables

// Using reserved words

fn main() {
    let let=4;
    println!("{}", let);
}
// r#

fn main() {
    let r#let=4;
    println!("{}", r#let);
}

Chapter 2

Data Types

Data Types

Rust is a statically typed language, meaning, it must know the type of all variables at compile time.

Variable Definition

Implicit Definition

Unlike other languages like C++ and Java, Rust can infer the type from the type of value assigned to a variable.

let variablename = value

Explicit Definition

let variablename:datatype = value

Integer

Fixed Size Integers

  • i8: The 8-bit signed integer type.
  • i16: The 16-bit signed integer type.
  • i32: The 32-bit signed integer type.
  • i64: The 64-bit signed integer type.
  • u8: The 8-bit unsigned integer type.
  • u16: The 16-bit unsigned integer type.
  • u32: The 32-bit unsigned integer type.
  • u64: The 64-bit unsigned integer type.

Variable Size Integers

The integer type in which the particular size depends on the underlying machine architecture.

  • isize: The pointer-sized signed integer type.
  • usize: The pointer-sized unsigned integer type.
// see the smallest and biggest numbers,you can use MIN and MAX 
// after the name of the type

fn main() {
    println!("The smallest i8 is {} and the biggest i8 is {}.", i8::MIN, i8::MAX); // hint: printing std::i8::MIN means "print MIN inside of the i8 section in the standard library"
    println!("The smallest u8 is {} and the biggest u8 is {}.", u8::MIN, u8::MAX);
    println!("The smallest i16 is {} and the biggest i16 is {}.", i16::MIN, i16::MAX);
    println!("The smallest u16 is {} and the biggest u16 is {}.", u16::MIN, u16::MAX);
    println!("The smallest i32 is {} and the biggest i32 is {}.", i32::MIN, i32::MAX);
    println!("The smallest u32 is {} and the biggest u32 is {}.", u32::MIN, u32::MAX);
    println!("The smallest i64 is {} and the biggest i64 is {}.", i64::MIN, i64::MAX);
    println!("The smallest u64 is {} and the biggest u64 is {}.", u64::MIN, u64::MAX);
    println!("The smallest i128 is {} and the biggest i128 is {}.", i128::MIN, i128::MAX);
    println!("The smallest u128 is {} and the biggest u128 is {}.", u128::MIN, u128::MAX);

}

Explicit Declaration

fn main() {
    //explicitly define an integer
    let a:i32 = 24;
    let b:u64 = 23;
    let c:u8 = 26;
    let d:i8 = 29;
    //print the values
    println!("a: {}", a);
    println!("b: {}", b);
    println!("c: {}", c);
    println!("d: {}", d);
    
}

Alternate Way to Declare

// Alternate Way

fn main() {
    let small_number: u8 = 10;
    let small_number1 = 10u8; // 10u8 = 10 of type u8 (no space inbetween 10 and u8)
    let big_number = 100000000i32;
    let big_number1 = 100_000_000i32; // adds clarity to numbers
     let big_number2 = 100_____000________000i32;  //to demonstrate multiple ___
}

Type Inference

fn main() {
    //implicitly define an integer
    let a = 21; 
    let b = 1;
    let c = 54;
    let d = 343434;
    //print the variable
    println!("a: {}", a);
    println!("b: {}", b);
    println!("c: {}", c);
    println!("d: {}", d);
    
}

When not declared, the Default integer type inferred by Rust is i32

// 
fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}

fn main() {
    let a = 5;
    let b = 3.14;
    print_type_of(&a);
    print_type_of(&b);
}    

Floating Point

Floats are numbers with decimal points.

  • f32: The 32-bit floating point type.
  • f64: The 64-bit floating point type.

It doesn't support 16 or 128

 fn main() {
    //explicitly define a float type
    let f1:f32 = 32.9;
    let f2:f64 = 6789.89;
    let f3:fsize = 3.141414141414;
    println!("f1: {}", f1);
    println!("f2: {}", f2);
    println!("f3: {}", f3);
    
    //implicitly define a float type
    let pi = 3.14;
    let e = 2.17828;
    println!("pi: {}", pi);
    println!("e: {}", e);
}

Values are the same, but they are not equal.

// Adding f32 + f64

fn main() {
    let my_float: f64 = 5.0; // This is an f64
    let my_float2: f32 = 5.0; // This is an f32

    let my_float3 = my_float + my_float2;️
    println!("{}",my_float3);
}

So how to fix it?

// Adding f32 + f64 the right way

fn main() {
    let my_float: f64 = 5.0; // This is an f64
    let my_float2: f32 = 5.0; // This is an f32

    let my_float3 = my_float + my_float2 as f64;
    println!("{}",my_float3);
}

If not declared, the Default type is f64

// What is size of my_other_float variable?
// Adding f32 with f64 will it work or fail?
// Rust is smart, 
// since it is doing addition with f32, it will default it to f32 instead of f64

fn main() {
    let my_float: f32 = 5.0;
    let my_other_float = 8.5; 

    let third_float = my_float + my_other_float;
}

Boolean

true
false
fn main() {
    //explicitly define a bool
    let is_bool:bool = true;
    println!("explicitly_defined: {}", is_bool);
    
    // implicit
    let a = true;
    let b = false;
    println!("a: {}", a);
    println!("b: {}", b);

}

Expression result

fn main() {
    // get a value from an expression
    let c = 10 > 2;
    println!("c: {}", c);
}

Char & Strings

The value assigned to a char variable is enclosed in a single quote('') .

Unlike some other languages, a character in Rust takes up 4 bytes rather than a single byte. It does so because it can store a lot more than just an ASCII value like emojis, Korean, Chinese, and Japanese characters.

fn main() { 
    // implicitly & explicitly define
    let char_2:char = 'a';
    let char_3 = 'b';
    println!("character2: {}", char_2);
    println!("character3: {}", char_3);
}

String Literal

Used when the value of the string is known at compile time. Literals are set of characters that are hardcoded to a variable at compile time. String literals are found in the module std::str

String literals are stored in the Stack portion of the memory so retrieval is fast.

fn main() {
    // explicitly define 
    let str_1:&str = "Rust Programming";
    println!("String 1: {}", str_1);
 
    // implicitly define
    let str_2 = "Rust Programming";
    println!("String 2: {}", str_2);
}

String Object

String objects are dynamic and can be changed during runtime.

A String object is allocated in the heap memory. Its slower but has more features.

String::new() - Creates an empty string.

String::from() - Default value passed as parameter.

fn main(){
   let empty_string = String::new();
   println!("length is {}",empty_string.len());

   let content_string = String::from("Rachel Green");
   println!("length is {}",content_string.len());
}

String Operations

variable.push() - to push a single character

// Push Single Character

fn main(){
   let mut name1 = String::from("Hello");
   println!("{}",name1);
   name1.push('!');
   println!("{}",name1);
}

variable.push_str() - to push a set of characters

// Push a string

fn main(){
   let mut name1 = String::from("Hello");
   println!("{}",name1);
   name1.push_str("World");
   println!("{}",name1);
}

variable.replace("","")

fn main(){
   let name1 = String::from("Hello!");
   let name2 = name1.replace("Hello","Howdy");    //find and replace
   println!("{}",name2);
}

Convert String Literal to String Object (to_string())

fn main(){
   let name1 = "Hello!".to_string();              //String object
   let name2 = name1.replace("Hello","Howdy");    //find and replace
   println!("{}",name2);
}

Convert String Object to String Literal (as_str())

fn main() {
    let name1 = String::from("hello");
    let name2 = name1.as_str();
    println!("{},{}", name1, name2);
}

Script to find the data type

fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}

fn main() {
    let name = "StringSample";
    let name1 = String::from("Hello");
    print_type_of(&name);
    print_type_of(&name1);
}    

String based programs

Assignment - Explain the logic behind these working examples.

For example, you need to explain why &str2 ? why not str2.

what is the use of collect()

Concatenation

// concatenation

fn main() {
  let str1 = "Hello".to_string();
  let str2 = " world".to_string();
  let string = str1 + &str2;
  println!("{}", string);
}

String Reverse

// Reverse String

fn main() {
    let s = "Hello World";
    let t: String = s.chars().rev().collect();
    println!("{}", t);
}

Check Palindrome

// Palindrome

fn main() {
    let s = "rotator";
    let t: String = s.chars().rev().collect();
    
    if s == t {
        println!("Palindrome")
    }
    else{
        println!("Not Palindrome")
    }
}

String Padding

 // String Padding

fn main() {
    let s = "pizza";
    
    println!("{s:-^30}");
    println!("{s:*<30}");
    println!("{s:#>30}");
}

Arrays

An array is a homogenous sequence of elements.

A Collection of values of the same type is to be stored in a single variable.

Fixed length & Length known at compile time.

By default, the first element is always at index 0.

By default, arrays are immutable.

Src:Educative.io

Define an Array

#[allow(unused_variables, unused_mut)]
fn main() {
   //define an array of size 4
   let arr:[i32;4] = [1, 2, 3, 4]; 
   // initialize an array of size 4 with 0
   let arr1 = [0 ; 4]; 
}

Access Array element

fn main() {
   //define an array of size 4
   let arr:[i32;4] = [1, 2, 3, 4]; 
   //print the first element of array
   println!("The first value of array is {}", arr[0]);
   // initialize an array of size 4 with 0
   let arr1 = [10; 14]; 
   //print the first element of array
   println!("The first value of array is {}", arr1[0]);
}

Mutable Arrays

fn main() {
    //define a mutable array of size 4
    let mut arr:[i32;4] = [1, 2, 3, 4]; 
    println!("The value of array at index 1: {}", arr[1]);
    arr[1] = 9;
    println!("The value of array at index 1: {}", arr[1]);
}

Print Array

Using Loop or using debug trait

fn main() {
    //define an array of size 4
    let arr:[i32;4] = [1, 2, 3, 4]; 
    //Using debug trait
    println!("\nPrint using a debug trait");
    println!("Array: {:?}", arr);
}

Get Array Length

fn main() {
    //define an array of size 4
    let arr:[i32;4] = [1, 2, 3, 4]; 
    // print the length of array
    println!("Length of array: {}", arr.len());
}

Slice

Slice is basically a portion of an array. It lets you refer to a subset of a contiguous memory location. But unlike an array, the size of the slice is not known at compile time.

A slice is a two-word object, the first word is a data pointer and the second word is a slice length.

Data pointer is a programming language object that points to the memory location of the data, i.e., it stores the memory address of the data.

fn main() {
    //define an array of size 4
    let arr:[i32;4] = [1, 2, 3, 4]; 
    //define the slice
    let slice_array1:&[i32] = &arr;
    let slice_array2:&[i32] = &arr[0..2];
    let slice_array2:&[i32] = &arr[0..];
    // print the slice of an array
    println!("Slice of an array: {:?}", slice_array1);
    println!("Slice of an array: {:?}", slice_array2);
}

Tuples

Tuples are heterogeneous sequences of elements, meaning, each element in a tuple can have a different data type. Just like arrays, tuples are of a fixed length.

Define a Tuple

A tuple can be defined by writing let followed by the name of the tuple and then enclosing the values within the parenthesis.

Implicit Inference

Explicit Inference

#[allow(unused_variables, unused_mut)]
fn main() {
    //define a tuple
    let person_data = ("Rachel", 30, "50kg", "5.4ft");
    // define a tuple with type annotated
    let person_data2 : (&str, i32, &str, &str) = ("Ross", 31, "55kg", "5.8ft");
    
    println!("{}",person_data.0);
    println!("{}",person_data.1);
    println!("{}",person_data2.0);
}

Assign Tuple value to individual variables

#[allow(unused_variables, unused_mut)]
fn main() {
    //define a tuple
    let person_data = ("Rachel", 30, "50kg", "5.4ft");
    
    let (name,age,wt,ht) = person_data;
    
    println!("{}",name);
    println!("{}",age);
    println!("{}",wt);
    println!("{}",ht);
}

Mutable Tuples

fn main() {
    //define a tuple
    let mut person_data = ("Rachel", 30, "50kg", "5.4ft");
    //print the value of tuple
    println!("The value of the tuple at index 0 and index 1 are {} {}", person_data.0, person_data.1);
    //modify the value at index 0
    person_data.0 = "Monica";
    //print the modified value
    println!("The value of the tuple at index 0 and index 1 are {} {}", person_data.0, person_data.1);
}

Print using Debug Trait

fn main() {
    //define a tuple
    let mut person_data = ("Rachel", 30, "50kg", "5.4ft");
    //print the value of tuple
    println!("Tuple - Person Data : {:?}", person_data);
}

Constants

Constant variables are ones that are declared constant throughout the program scope, meaning, their value cannot be modified. They can be defined in the global and local scope.

All letters should be UPPER CASE and words separated by underscore (_)

Example:

ID_1

ID_2

const ID_1: i32 = 4; // define a global constant variable

fn main() {
    const ID_2: u32 = 3; // define a local constant variable
    println!("ID:{}", ID_1); // print the global constant variable
    println!("ID:{}", ID_2); // print the local constant variable
}

How is it different from let immutable variables

ConstImmutable Let
declared using constdeclaured using let
mandatory to define the data typedata type declaration is optional
The Value of const is set before running the programVariable can store the result at runtime
Const cannot be shadowedlet can be shadowed

Unit Type

The () type, also called “unit”. The () type has exactly one value () , and is used when there is no other meaningful value that could be returned.

The empty tuple type, () , is called “unit”, and serves as Rust's void type (unlike void , () has exactly one value, also named () , which is zero-sized).

Example 1

fn main() {
    let my_tuple = (42, "hello", ());
    println!("Tuple: {:?}", my_tuple);
}

In this example, the tuple my_tuple contains an integer, a string, and a unit type (). When you run the code, it will print:

Tuple: (42, "hello", ())

Example 2

fn main() {
    let result = do_nothing();
    println!("Result of do_nothing: {:?}", result);
}

fn do_nothing() -> () {
    // This function does nothing and returns unit type
}

& and *

& - Reference

* - Dereference
// & and *
// Reference and Dereference

fn main() {
    let a = 10;
    let b = &a;
    
    //Printing the value of a and memory reference of a
    
    println!("{} - {:p}",a, &a);
    
    // dereferencing b (10) and value of b (memory location of a)
    println!("{} - {:p}",*b, b);
    
    // dereferencing memory reference of a, referencing deference of b
    println!("{} - {:p}",*(&a), &(*b));
}

Expression

 // Expression

fn main() {
    let x = 5u32;

    let y = {
        //demonstrate with semicolons and without semicolon
        let x_squared = x * x;
        let x_cube = x_squared * x;

        // This expression will be assigned to `y`
        x_cube + x_squared + x
    };

    let z = {
        2 * x
    };
    
    let zz = {
        // The semicolon suppresses this expression and `()` is assigned to `z`
        2 * x;
    };

    println!("x is {:?}", x);
    println!("y is {:?}", y);
    println!("z is {:?}", z);
    println!("zz is {:?}", zz);
}

Operators

Operators

Binary Operators

Operators that deal with two operands

  • Arithmetic Operators
  • Logical Operators
  • Comparison Operators
  • Assignment Operator
  • Compound Assignment Operator
  • Bitwise Operators
  • Typecast Operators

Unary Operators

The Operators that act upon a single operand, for example, Negation Operator.

Binary Operators

Arithmetic Operators

+

-

*

/ (Division)

% (Modulus)

fn main() {
    let a = 4;
    let b = 3;
    
    println!("Operand 1:{}, Operand 2:{}", a , b);
    println!("Addition:{}", a + b);
    println!("Subtraction:{}", a - b);
    println!("Multiplication:{}", a * b);
    println!("Division:{}", a / b);
    println!("Modulus:{}", a % b);
}

Logical Operators

&& (AND Operator)

|| (OR Operator)

! (Not Operator)

AND and OR are known as LAZY Boolean expressions.

LHS is evaluated first, and based on the outcome RHS is evaluated.

In the case of AND, if LHS is False, then there is no need to evaluate the RHS.

In the case of OR, if LHS is True, then there is no need to evaluate the RHS.

fn main() {
  let a = true;
  let b = false;
  println!("Operand 1:{}, Operand 2:{}", a , b);
  println!("AND:{}", a && b);
  println!("OR:{}", a || b);
  println!("NOT:{}", ! a);
}

Comparison Operator

>, <, <=, >=, ==, !=

fn main() {
    let a = 2;
    let b = 3;
    println!("Operand 1:{}, Operand 2:{}", a , b);
    println!("a > b:{}", a > b);
    println!("a < b:{}", a < b);
    println!("a >= b:{}", a >= b);
    println!("a <= b:{}", a <= b);
    println!("a == b:{}", a == b);
    println!("a != b:{}", a != b);
}

Bitwise Operator

Bitwise operators work on the binary representation of numbers. They're often used for:

  1. Bit Manipulation: Flipping bits, setting bits to 1 or 0.
  2. Optimization: Faster arithmetic operations like multiplication or division by powers of 2.
  3. Masking: Extracting specific bits from a number.
  4. Encoding & Decoding: Data compression or encryption techniques.
  5. Networking: IP address manipulation, subnet masking.
x = 57  # 0011 1001 in binary
mask = 15  # 0000 1111 in binary
result = x & mask  # 0000 1001, or 9 in decimal

Common use cases with Rust

  1. Memory Management: Manipulating individual bits for custom allocators.
  2. File I/O: Reading and writing binary files, especially in low-level systems programming.
  3. Graphics: Bitwise operations are used in image processing for tasks like masking.
  4. Cryptography: Implementing cryptographic algorithms often involves bitwise manipulation.
  5. Hardware Interaction: Directly interacting with hardware often requires setting specific bits.
fn main() {
    let mut flags = 0b0000_0101;
    let mask = 0b0000_1000;
    flags |= mask;  // Sets the bit at position 3 to 1
    println!("{}",flags);
}

& - Bitwise AND

| - Bitwise OR

^ - Bitwise XOR

! - Bitwise NOT

<< - Left Shift Operator

>> - Right Shift Operator

Src: Educative.io

Unary Operators

Unary operators in Rust are operators that operate on a single operand. They perform various operations such as negation, dereferencing, and borrowing. Common unary operators in Rust include:

    • (Negation): Negates a numerical value.
  • ! (Logical NOT): Inverts a boolean value.
    • (Dereference): Dereferences a pointer, allowing access to the value it points to.
  • & (Borrow): Creates a reference to a value.
  • &mut (Mutable Borrow): Creates a mutable reference to a value.

Examples

Negation (-):

fn main(){
    let x = 5;
    let y = -x;
    println!("y: {}", y);  // Output: y: -5
    }

Logical NOT (!):

fn main(){
    let a = true;
    let b = !a;
    println!("b: {}", b);  // Output: b: false
}

Dereference (*):

fn main(){
    let x = 10;
    let y = &x;
    let z = *y;
    println!("z: {}", z);  // Output: z: 10
}

Borrow (&):

fn main(){
    let s = String::from("hello");
    let r = &s;
    println!("r: {}", r);  // Output: r: hello
}

Mutable Borrow (&mut):

fn main(){
    let mut s = String::from("hello");
    {
        let r = &mut s;
        r.push_str(", world");
    }
    println!("s: {}", s);  // Output: s: hello, world
}

Flow of Control

if..elseif..else construct

fn main() {
      //define a variable 
      let learn_language="Rust";
      
      // if..elseif..else construct 
      
      if learn_language == "Rust" { 
         println!("You are learning Rust language!");
      }
      else if learn_language == "Java" { 
         println!("You are learning Java language!");
      }
      else {
         println!("You are learning some other language!");
      } 
}

Nested IF

// Nested If Block

fn main() {
    //define a variable 
    let learn_language1 = "Rust";
    let learn_language2 = "Java";
    // outer if statement
    if learn_language1 == "Rust" {  // inner if statement
        if learn_language2 == "Java"{
              println!("You are learning Rust and Java language!");
        }
    }
    else {
      println!("You are learning some other language!");
    } 
}

If Expression

// If Expression

fn main() {
    //define a variable  
    let learn_language = "Rust";
    // short hand construct
    let res= if learn_language == "Rust" {"You are learning Rust language!"} else {"You are learning some other language!"};
    println!("{}", res);
}

Q & A

Q1: What is the output of the following?

// Qn 1

fn main() {
   let age=23; 
   if age >=21{ 
      println!("Age is greater than 21");
   }
    else if age <21{
       println!("Age is less than 21");
    }
    println!("Value Printed");
}

Q2: Which If block is executed?

fn main() {
   let age=23; 
   let play=true; 
   let activity="Tennis" ;
   if age >=21 && play==false && activity=="Tennis"{ 
     println!("Age is greater than 21");
     println!("You are not allowed to play");
     println!("The sport is {}",activity);
   }
   else if  age >=21 && play==true && activity=="Tennis"{ 
     println!("Age is greater than 21");
     println!("You are allowed to play");
     println!("The sport is {}",activity);
   }
   else if age <21 && play==false && activity=="Tennis"{
     println!("Age is less than 21");
     println!("You are allowed to play");
     println!("The sport is {}",activity);
   }
   else {
     println!("Value Printed");
   }
}

Q3: What is the output of the following code?

fn main() {
  let age = 23; 
  let play = true; 
  let activity="Baseball" ;
  if age >= 21 && play==true || activity == "Tennis" { 
    println!("Age is greater than 21");
    println!("You are allowed to play");
    println!("The sport is {}",activity);
  }
  else if  age >= 21 && play == true && activity == "Tennis"{ 
    println!("Age is greater than 21");
    println!("You are allowed to play");
    println!("The sport is {}",activity);
  }
  else if age <21 && play == false && activity == "Tennis"{
    println!("Age is less than 21");
    println!("You are allowed to play");
    println!("The sport is {}",activity);
  }
  else{
    println!("Value Printed");
  }
 }

Match

Match is similar to Switch Case in other languages.

fn main() {
    let number = 34;
    // TODO ^ Try different values for `number`

    println!("Tell me about {}", number);
    match number {
        // Match a single value
        1 => println!("One!"),
        // Match several values
        2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
        // TODO ^ Try adding 13 to the list of prime values
        // Match an inclusive range
        13..=19 => println!("A teen"),
        // we can bind the matched number to a variable
        matched_num @ 10..=100 => {
            println!("found {} number between 10 to 100!", matched_num);
        }
        // Handle the rest of cases
        _ => println!("Ain't special"),
        // TODO ^ Try commenting out this catch-all arm
    }

    let boolean = true;
    // Match is an expression too
    let binary = match boolean {
        // The arms of a match must cover all the possible values
        false => 0,
        true => 1,
        // TODO ^ Try commenting out one of these arms
    };

    println!("{} -> {}", boolean, binary);
}

Tuples with Match

fn main() {
    let triple = (0, -2, 3);
    // TODO ^ Try different values for `triple`

    println!("Tell me about {:?}", triple);
    // Match can be used to destructure a tuple
    match triple {
        // Destructure the second and third elements
        (0, y, z) => println!("First is `0`, `y` is {:?}, and `z` is {:?}", y, z),
        (1, ..)  => println!("First is `1` and the rest doesn't matter"),
        (.., 2)  => println!("last is `2` and the rest doesn't matter"),
        (3, .., 4)  => println!("First is `3`, last is `4`, and the rest doesn't matter"),
        // `..` can be used to ignore the rest of the tuple
        _      => println!("It doesn't matter what they are"),
        // `_` means don't bind the value to a variable
    }
}

Iterations a.k.a Loops

There are 3 types of loops in Rust

While Loop

Loops as long as the condition is True, exit when the condition if False.

fn main() {
    let mut i = 0;
    while i != 6 {
        i += 1;
        println!("inside loop value of i : {i}")
    }
    println!("finally i is {}", i);
}

For Loop

// Left side Inclusive, Right side exclusive

fn main() {
    for x in 0..5 {
        println!("{}", x);
    }

// By adding =, both sides are inclusive

    for x in 0..=5 {
        println!("{}", x);
    }
}

Loop

fn main() {
    let mut x = 0;
    loop {
        x += 1;
        if x == 5 {
            break;
        }
    }
    println!("{}", x);
}
// Break with message

fn main() {
    let mut x = 0;
    let v = loop {
        x += 1;
        if x == 5 {
            break "found the 5";
        }
    };
    println!("from loop: {}", v);
}

Functions

Like other programming languages, functions are the basic building blocks of readable, maintainable, and reusable code.

In Rust, functions can be created before or after the main routine.

Simple Function

// Simple Function

fn main(){
   //calling a function
   hello();
}

fn hello(){
   println!("Hi");
}

Return a Value

// Return Value
// Demonstrate the same with the return value (35000.00*5.0*6.4/100.00)

fn main(){
   println!("Hi {}",calc_si());
}

fn calc_si()->f32 {
   35000.00*5.0*6.4/100.00
}

Call By Value

// Call by Value

fn main(){
   let no:i32 = 5;
   changeme(no);
   println!("Main Function:{}",no);
}

fn changeme(mut param_no: i32) {
   param_no = param_no * 0;
   println!("Inside the Function :{}",param_no);
}

Call By Reference

// Call by Reference

// Call by Reference

fn main() {
    let mut no: i32 = 5;
    println!("Main fucntion initial value :{} -> {:p}", no,&no);
    changeme(&mut no);
    println!("Main function final value  is:{} -> {:p}", no,&no);
}

fn changeme(param_no: &mut i32) {
    println!("Changeme function initial value :{} -> {:p}", *param_no,&(*param_no));
    *param_no = 0; //de reference
    println!("Changeme function final value :{} -> {:p}", *param_no,&(*param_no));
}

Call By Reference

Unit Tests

Calculator

fn add(a: f32, b: f32) -> f32 {
    a + b
}

fn sub(a: f32, b: f32) -> f32 {
    if a < b {
        panic!("first value cannot be less than the second value");
    } else {
        a - b
    }
}

#[allow(dead_code)]
fn mul(a: f32, b: f32) -> f32 {
    a * b
}

#[allow(dead_code)]
fn div(a: f32, b: f32) -> f32 {
    a / b
}

fn main() {
    let a: f32 = 17.0;
    let b: f32 = 33.0;
    let op = "-";
    let mut result: f32 = 0.0;

    if op == "+" {
        result = add(a, b);
    } else if op == "-" {
        result = sub(a, b)
    }

    println!("{result}");
}

#[test]
fn test_add() {
    assert!(add(20.0, 10.0) == 30f32);
}

#[test]
fn test_add1() {
    assert!(add(10.0, 20.0) == 30f32);
}

#[test]
#[should_panic(expected = "cannot be less")]
fn test_sub() {
    assert!(sub(10.0, 20.0) == -10.0f32);
}

#[test]
fn test_sub1() {
    assert!(sub(20.0, 10.0) == 10.0f32);
}

#[test]
#[ignore]
fn test_sub2() {
    assert!(sub(20.0, 10.0) == 10f32);
}

How to Test the code

cargo test

How to Run the code

cargo run

Assert Macros

// Different assert macros

let result = add(2,2);

assert!(result == 4);
assert!(result == 4,"Expected 4; returned result is {}",result);

-----------

let result = add(2,2);

assert_eq!(result,4);

assert_eq!(result,4,"Expected 4; returned result is {}", result);

------------

Similar to the previous one, checks for NOT Equalto.
assert_ne!()

Control Cargo Tests

To test all the Test Cases

cargo test

Test Arguments

cargo test [arguments1] -- [arguments2]

>arguments1 are the arguments for test utility like  help

cargo test --help

>arguments2 are the arguments for the application it's testing.


By default, cargo displays detailed output for failed test cases. To see the standard output for Success or Failure tests

cargo test -- --show-output

Make sure there is no space between --show-output 

Parallel Test Execution

By default, tests run in parallel by making use of the multi-core architecture.

In some situations (like file handling) there will be a race condition. To avoid that we can make them execute in sequential.

// Runs on single thread

cargo test -- --test-threads=1

Run test by name

cargo test <testname>

or

cargo test <string> 

cargo runs all tests containing the string in test name.

Ignore specific tests

#[test]
#[ignore]

adding this will ignore the test

Run ignored tests only

cargo test -- --ignored

Chapter 3

Memory Management

Stack - Heap

Src: Adafruit.com

We will learn the concept of memory management and how Rust can guarantee memory safety without a Garbage collector.

Like most programming languages, Rust stores data in three different structure parts of your computer memory.

Static/Data Memory

For data that is fixed in size and static (i.e. always available throughout the life of the program).

println!("Hello");

This text's bytes are only ever read from one place and therefore can be stored in this region. Compilers make lots of optimizations with this kind of data, and they are generally considered very fast to use since locations are known and fixed.

Program Binary

Static Variables

String Literals

Img Src: OpenGenus

Stack

Last In First Out

For data that is declared as variables within a function. The location of this memory never changes for the duration of a function call; because of this compilers can optimize code so stack data is very fast to access.

Function Arguments

Local Variables

Size is known at Compile time

let a:i32 = 100;

Rust knows this will take 32 bits of memory.

So the variable is stored in Stack Memory.

Automatic cleanup, when the function returns.

Example: Courtesy mit.edu

fn foo() {
    let y = 5;
    let z = 100;
}

fn main() {
    let x = 42;

    foo();
}
AddressNameValue
0x42

After calling the function foo()

AddressNameValue
2z100
1y5
0x42

Note: Memory address is taking into account DATA TYPE SIZE. It's just a representation.

After foo() gets executed, control transfers to main, and the values are deallocated automatically.

AddressNameValue
0x42

stateDiagram-v2
    [*] --> Main
    state Main {
        [*] --> PushMainFrame
        PushMainFrame: Push frame for main()
        PushMainFrame --> AllocateX: x = 42
        AllocateX --> CallFoo: Call foo()
        CallFoo --> PushFooFrame: Push frame for foo()
        state PushFooFrame {
            [*] --> AllocateY: y = 5
            AllocateY --> AllocateZ: z = 100
        }
        PushFooFrame --> PopFooFrame: Pop frame for foo()
        PopFooFrame --> ReturnFoo: foo() returns
        ReturnFoo --> PopMainFrame: Pop frame for main()
        PopMainFrame --> [*]
    }
    Main --> [*]

Copy Trait

    fn main() {
    // i32 is a simple type and are normally stored on the stack.
    // copy trait

    let x = 42;
    let y = x; 

    // The value bound to x is Copy, so no error will be raised.
    println!("{:?}", (x, y));

    // The value bound to x is Copy, so no error will be raised.
    println!("{:p},{:p}", &x, &y);
}

Heap

For data that is created while the application is running. Data in this region may be added, moved, removed, resized, etc. Because of its dynamic nature, it's generally considered slower to use, but it allows for much more creative usage of memory. When data is added to this region, we call it an allocation. When data is removed from this section, we call it deallocation.

Example: Vector, String

fn main(){
    let s1=String::from("hello");
    println!("{}",s1)
}
// Move Trait (Heap)

fn main() {
    let mut name = String::from("Hello World");
    println!("Memory address of name: {},{:p} \n", name,&name);

    //moving

    let name1 = name;
    println!("Memory address of name1: {},{:p} \n", name1,&name1);
    
    //println!("Memory address of name: {},{:p} \n", name,&name);

    // Setting up another Value for the variable name

    name = String::from("Dear World");
    println!("Memory address of name: {},{:p}\n", name,&name);
}

Example: pdf (stack) - printed book (heap)

// Copy Trait (Stack - because of using String Literal)

fn main() {
    let name = "Hello World";
    println!("Memory address of name: {},{:p} \n", name,&name);

    //Copying

    let name1 = name;
    println!("Memory address of name1: {},{:p} \n", name1,&name1);
    
    println!("Memory address of name: {},{:p} \n", name,&name);
}

Ownership

Ownership

It's a unique & critical aspect of Rust.

In C/C++ there is no concept called a Garbage Collector. (which frees up the unused memory).

  • But they are very fast and performant.
  • Developers need to work to free up memory. (remember free() function?)

In other languages such as Python, Java, and Go we have Garbage Collector

  • Relatively slower
  • Developers can focus only on business logic.

Rust Ownership System

  • Best of both worlds.
  • The Compiler replaces most of GC's responsibilities.
  • Determines at compile time when memory is allocated and deallocated.
  • Requires developers to code in specific ways. (All the checks and balances)

Rules of Ownership

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

{:p} - Pointer Trait, used to print the memory location of variables.

// Demonstrate Shadowing

fn main() {
    let x = String::from("Rachel");
    println!("Memory address of x: {:p}", &x);
    
    // new x is created in another memory location
    let x = 5;
    println!("Memory address of x: {:p}", &x);
}

Borrowers

Access data without taking ownership of it by creating references using the borrow operator (&)

We are familiar with this operator in the call by reference.

// Print Name - The second print fails because the name transferred its ownership to p_name

fn printname(p_name:String){
    println!("{name}")
}

fn main() {
    let name:String = String::from("Rachel");
    printname(name);
    printname(name);
}

How to fix it?

Borrowing Concept

// Instead of passing the actual value, passing the Reference / Borrow operator

fn printname(p_name:&String){
    println!("{p_name}")
}

// by default len() returns usize.

fn get_length(p_name: &String) -> i8 {
    let name_length:i8 = p_name.len() as i8;
    name_length
}

fn main() {
    let name:String = String::from("Rachel");
    printname(&name);
    printname(&name);
    let name_length = get_length(&name);
    println!("Length is {name_length}");
}

Using String.clone()

// String.clone()

fn printname(name:String){
    println!("{},{:p}",name,&name)
}

fn main() {
    let name:String = String::from("Rachel");
    printname(name.clone());
    printname(name.clone());
    printname(name);
}

Owner Borrower Stack Heap

// Owner - Borrower - Stack - Heap - Slices

fn main() {
    //Create a String Object o with the value Rachel Green

    let o = String::from("Rachel Green");
    println!("Value of the variable 'o' is {o}");

    // .len() - returns the actual length
    println!("\nString Length: {}", o.len());
    // .capacity() - memory reserved for String Object
    println!("String Capacity: {}", o.capacity());

    // as_ptr(): Returns a raw pointer to the underlying data in this cell.
    println!("Heap location of {o} starts at {:p}", o.as_ptr());

    ////////////

    //Slicing variable o and get the second portion of the variable, that is, Green
    let s = &o[7..12];

    println!("\nValue of the variable 's' is {s}");
    println!("Heap location of {s} starts at {:p}", s.as_ptr());

    // Get the stack location of variables o and s
    println!("\nStack location of owner 'o' {:p}", &o);
    println!("Stack location of owner 's' {:p}", &s);

    //////////

    // o is the owner, and b is the borrower.
    let b = &o;

    println!("\nBorrower 'b' is {}", b);
    println!("Borrower 'b' points to Owner 'o' {:p}", b);
    println!("Stack location of borrower 'b' {:p}", &b);
    println!("Value borrower 'b' pointing to 'o' location {:p}",b.as_ptr());
}

Borrowing References

Borrowing Immutable References

There are few rules when it comes to borrowing. Let's take a quick look at them.

// Cannot change as it's not a mutable reference

fn changeme(param_msg: &String) {
    param_msg.push_str(" Green")
}

fn main() {
    let msg = String::from("Rachel");
    changeme(&msg);
}

Example: Borrow a book from the library; you cannot write anything. Just read from it.

Borrowing Mutable References

// Mutable reference

fn changeme(param_msg: &mut String) {
    param_msg.push_str(" Green")
}

fn main() {
    let mut msg = String::from("Rachel");
    changeme(&mut msg);
    println!("{}", msg);
}

Example: Borrow a book from a friend with permission to highlight or underline important items.

Immutable and Mutable Borrow

// Immutable Borrow - Stack

fn main() {
    let x = 5;
    
    // Immutable borrow
    let y = &x;
    // *y += 1;
    println!("Value of y: {}", y);
   

}

Immutable Borrow: Allows read-only access to a value. Multiple immutable borrows can coexist, but they cannot coexist with a mutable borrow.

// Mutable Borrow - Stack

fn main() {
    let mut x = 5;
    println!("Value of x: {}", x);
    
    // Mutable borrow
    let z = &mut x;
    *z += 1;
    println!("Value of x: {}", x);
}

Mutable Borrow: Grants read-write access to a value. Only one mutable borrow is allowed at a time, and no immutable borrows can coexist.


Immutable and Mutable Borrow

Rule: Immutable borrow should always be used in the code after the mutable borrow because whatever modifications are done by mutable borrow should not affect immutable borrow.

That’s why they always use immutable borrows after mutable borrows.

fn main() {
    let mut x = 5;
    
    // Mutable borrow
    let z = &mut x;
    *z += 1;
    
    println!("Value of z: {}->{:p}", z, &z);
    
    println!("Value of x: {}->{:p}", x, &x);
    
    // immutable borrows
    let y1 = &x;
    println!("Value of y1: {}->{:p}", y1, &y1);
    
    // Flip the immutable and mutable and print the Immutable value after Mutable
  
}

Use this website to sort the Memory locations to understand how values are stored in Stack. You can sort them by ASC or DESC order.

https://www.rajeshvu.com/storage/emc/utils/general/sorthexnumbers

Borrow Checker

Exactly. When b has a mutable borrow of a, a temporarily loses its write privilege. You can't modify a or even read from it while b has an active mutable borrow. Rust's borrow checker enforces this to ensure memory safety.

If you try to access a while b has an active mutable borrow, Rust's borrow checker will complain. This ensures you don't have multiple mutable references to the same data, which could lead to data races and undefined behavior. The borrow checker enforces these rules at compile time for memory safety.

// Borrow Checker - String - Heap

fn main() {
    let mut a = String::from("Rachel");
    
    let b = &mut a;
    
    println!("variable 'b' initial value is {} stored at this {:p}", b,b.as_ptr());
    
    b.push_str(" Green");
    
    //println!("variable 'a' {}{:p}", a,a.as_ptr());
    
    println!("variable 'b' {} {:p}", b,b.as_ptr());
    
    // println!("variable 'a' {}{:p}", a,a.as_ptr());
    
    b.push_str(" !");
    
    println!("variable 'b' after update {},{:p}", b,b.as_ptr());
    
    println!("variable 'a' after update {} {:p}", a,a.as_ptr());
}

Example: The book owner can use the book only after the borrowed person has completed the work.

Multiple Mutable Borrowers

// Not allowed to have multiple Mutable Borrowers at the same time/scope

fn main() {
    let mut a = String::from("Rachel");
    
    let b = &mut a;
    let c = &mut a;
    
    println!("{}", b);
    println!("{}", c);
   
}

Example: Two friends borrow the same book to highlight the important items.

// Multiple Mutable Borrowers (different scope)

fn main() {
    let mut a = String::from("Rachel");
   
    let b = &mut a;
    println!("{}", b);

    let c = &mut a;
    println!("{}", c);
}

Example: 2 friends borrow the book one after another for writing.

For clarity's sake, the above code can be written this way, too.

// Multiple Mutable Borrowers (different scope)

fn main() {
    let mut a = String::from("Rachel");

    {    
       let b = &mut a;
        println!("{}", b);
    }

    let c = &mut a;
    println!("{}", c);
}

Immutable and Mutable

Mutable first, followed by Immutable

// Immutable and Mutable

fn main() {
    let mut a = String::from("Rachel");
    
    let c = &mut a;
    c.push_str("!");
    println!("c = {}", c);
    
    let b = &a;
    println!("b = {}", b);

Rules:

  1. Multiple Immutable references are allowed in the same scope.
  2. Immutable and Mutable references are not allowed in the same scope.
  3. Immutable and Mutable references are allowed in different scopes.

Dangling References

A dangling reference occurs when you have a reference that points to an invalid memory location, usually because the data it refers to has been deallocated or moved. In languages like C and C++, this can lead to undefined behavior.

Rust's ownership model is designed to eliminate this issue. The borrow checker ensures that references cannot outlive the data they point to, making dangling references impossible in safe Rust code.

// Dangling Reference

fn main() {
    let r;
    {
        let x = 42;
        r = &x;
    }
    println!("r: {}", r);  // This won't compile
}
// Dangling Reference

fn get_name() -> &String{
    let name = String::from("Rachel");
    &name
}

fn main() {
    let name:String = get_name();
    println!("new name is {name}");
}

The above code results in an error.

this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime

The function get_name returns a reference variable &name;

After the function is done, the variable goes out of scope, leading to a NULL Pointer reference.

This is allowed in other languages like C++, leading to memory issues.

How to solve it?

Remove the & from the return value and get_name() definition and return the variable.

fn get_name() -> String{
    let name = String::from("Rachel");
    name
}

fn main() {
    let name:String = get_name();
    println!("new name is {name}");
}

Iterator

Iteration is the process of looping through a set of values.

How its different from Loops?

Loops are Imperative. You must mention how often to loop over and follow Procedural-style programming.

Iterators are Declarative. This means it specifies what to do instead of how to do it and follows Functional Style programming.

Functional Programming:

Declarative Style: Developers describe what they want rather than specifying how step-by-step.

Pure Functions: Pure functions are functions that always return the same output for the same input, and they have no side effects. They don't modify any external state or data outside their scope, making them easier to reason about and less prone to bugs.

Looping through an array using traditional method.

fn main() {
    let ages = [27, 35, 40, 10, 19];
    let mut index = 0;

    while index < ages.len() {
        let age = ages[index];
        println!("Age = {:?}", age);
        index += 1;
    }
}
// Using Iterator

fn main() {
    let ages = [27, 35, 40, 10, 19];
    let ages_iterator = ages.iter();

    for age in ages_iterator {
        println!("Age = {:?}", age);
    }
}

Some(T): Indicates that there is a value, and it's of type T

We will discuss more about Some in next chapter. It's like you have Some(letter) in your mailbox.

The purpose is to replace the concept of Null and handle Null Safety. It also handles Type Safety.

fn main() {
    let ages = [27, 35, 40, 10, 19];
    let mut ages_iterator = ages.iter();
    
    println!("{:?}",ages_iterator.next());
    println!("{:?}",ages_iterator.next());
    println!("{:?}",ages_iterator.next());
    println!("{:?}",ages_iterator.next());
    println!("{:?}",ages_iterator.next());
    println!("{:?}",ages_iterator.next());

}
fn main() {
    let ages = [27, 35, 40, 10, 19];
    let mut ages_iterator = ages.iter();

    while let Some(x) = ages_iterator.next(){
        println!("{:?},{}",Some(x),x);
    }
}
fn main() {
    let ages = [27, 35, 40, 10, 19];
    let mut ages_iterator = ages.iter();

    // Looping thru Array
    for age in ages {
        println!("Age = {:?}", age);
    }

    println!("{:?}", ages_iterator.next());
    
    // Looping thru an Iterator. See the additional functionality it offers
    
    for age in ages_iterator {
        println!("Age = {:?}", age);
    }
}

Enumerate()

In an given Array, how to print the index and value?

fn main() {
    let fruits = ["apple", "banana", "cherry"];

    for i in 0..fruits.len() {
        println!("Index: {}, Fruit: {}", i, fruits[i]);
    }
}

Instead of handling i and index positions manually, Rust offers an easier technique using Enumerate().

Like iter() enumerate() is also a part of Functional programming.

In Rust, enumerate() is a method provided by the standard library that is used to iterate over an iterator (like a array, or any other data structure that implements the IntoIterator trait) and get both the index and the value at that index in each iteration.

Here are some use cases:

  1. Index Tracking: When you need to know the index of an element while iterating, enumerate() is handy.
  2. Conditional Logic: Sometimes, the logic inside a loop might depend on the element's index. For example, you might want to skip the first element.
  3. Debugging: When debugging, knowing the index of an element can help trace or log.
  4. Data Mapping: When you need to create a new data structure that relies on the index and value from an existing iterable.

The enumerator turns an iterator over elements.

fn main() {
    let fruits = ["apple", "banana", "cherry"];

    for (index, fruit) in fruits.iter().enumerate() {
        println!("Index: {}, Fruit: {}", index, fruit);
    }
}

Using multiple conditions and variables, a loop condition can also be more complex. For example, the for loop can be tracked using enumerate.

fn main() {
    for (i, j) in (100..120).enumerate() {
        println!("loop has executed {} times. j = {}", i, j);
    }
}

Ignoring Index position with an _

// _ is a generic placeholder.

fn main()  
{ 
    let my_array: [i32; 7] = [1i32,3,5,7,9,11,13]; 
    let mut value = 0i32; 
    for(_, item) in my_array.iter().enumerate() 
    { 
       value += item; 
    } 
    println!("{}", value); 
} 

Slices

Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection. A slice is a kind of reference, so it does not have ownership.

A string slice is a reference to part of a String, and it looks like this:

// String Slicing

fn main() {
    //define an array of size 4
    let arr:[i32;7] = [1, 2, 3, 4,5,6,7]; 
    
    //define the slice
    let slice_array1 = &arr;
    let slice_array2 = &arr[0..4];
    let slice_array3 = &arr[3..];
    
    // print the slice of an array
    println!("Value of slice_array1: {:?}", slice_array1);
    println!("Value of slice_array2: {:?}", slice_array2);
    println!("Value of slice_array3: {:?}", slice_array3);
}

Example 2

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {
    let s = String::from("hello world");
    let word = first_word(&s); // word will get the value 5
    println!("{}",word);
}

The string s is converted to bytes using the as_bytes() method for the purpose of iterating over individual bytes and finding the index of the first space character (b' ').

The reason for converting the string to bytes is that strings in Rust are encoded using UTF-8, which means that a single character (like 'é') can be represented by multiple bytes. If you were to iterate over the characters of the string directly, you might not get the correct index of the space character, especially if the string contains non-ASCII character.

// return value

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let s = String::from("hello world");
    let word = first_word(&s); // word will get the value 5
    println!("{}",word);
}

Chapter 4

Complex Datatypes

Type Alias

A type alias defines a new name for an existing type. Type aliases are declared with the keyword type.

  1. Improving Readability

Type aliases can make complex types easier to read and understand.

  1. Enhancing Maintainability

Type aliases can help centralize the definition of a type, making it easier to update the type across the codebase.

Point to remember

The first letter of the type should be in upper case.

// type alias

type Bannertype = u32;

fn main() {
    let mut id: Bannertype = 91615214;
    println!("{id}");
    id = 91615200;
    println!("{id}");
}

Another example

type Kilometers = i32;
type Meters = i32;

fn calculate_distance(distance: Kilometers) -> Meters {
    distance * 1000
}

fn main() {
    let distance: Kilometers = 5;
    let distance_in_meters: Meters = calculate_distance(distance);
    println!("Distance in meters: {}", distance_in_meters);
}



Vector Datatype

Dynamic Arrays

Unlike Arrays no need to initialize the size of the array.

// Vector

fn main(){
    let mut my_vec = Vec::new();
    my_vec.push("Rachel");
    my_vec.push("Monica");
    my_vec.push("Phoebe");
    
    println!("{:?}",my_vec);
       
}
fn main(){
    let mut my_vec = Vec::new();
    my_vec.push("Rachel");
    my_vec.push("Monica");
    my_vec.push("Phoebe");
    
    println!("{:?},{:p},{:p}",my_vec,&my_vec,my_vec.as_ptr());
       
}

Vector with Datatype

// This vector is initialized with i32 datatype.
// This code will result in error.

fn main(){
    let mut my_vec: Vec<i32> = Vec::new();
    
    my_vec.push("Rachel");
    my_vec.push("Monica");
    my_vec.push("Phoebe");
    
    println!("{:?}",my_vec);
    
}

// Storing String Literal in String Object

fn main(){
    let mut my_vec: Vec<String> = Vec::new();
    my_vec.push("Rachel");
    my_vec.push("Monica");
    my_vec.push("Phoebe");
   
    println!("{:?}",my_vec);  
}
// &str

fn main(){
    let mut my_vec: Vec<&str> = Vec::new();
    my_vec.push("Rachel");
    my_vec.push("Monica");
    my_vec.push("Phoebe");
    
    println!("{:?}",my_vec);
    
}
// String

fn main(){
    let mut my_vec: Vec<String> = Vec::new();
    my_vec.push("Rachel".to_string());
    my_vec.push("Monica".to_string());
    my_vec.push("Phoebe".to_string());
   
    println!("{:?}",my_vec);  
}

Vec Macro for initializing

// Vec macro

fn main(){
    //Using vec macro
    let my_vec = vec![2,4,6,8,10,12,14,16];
    
    let one = &my_vec[2..6];
    let two = &my_vec[2..];
    let three = &my_vec[..6];
    let four = &my_vec[..];
    
    println!("{:?}",one);
    println!("{:?}",two);
    println!("{:?}",three);
    println!("{:?}",four);
}

Capacity() vs Len()

// capacity() number of elements the vector can hold (without reallocating memory). This is usually larger than or equal to the number of elements currently in the vector.
// len() number of elements

fn main(){
    let mut my_vec: Vec<String> = Vec::new();
    my_vec.push("Rachel".to_string());
    my_vec.push("Monica".to_string());
    my_vec.push("Phoebe".to_string());

    println!("{:?}",my_vec);
    //Initial capacity was 4
    println!("{}",my_vec.capacity());
    println!("{}",my_vec.len());
    
    // now Rust is allocating space 100 more elements. Now the total capacity will be 103. 
    my_vec.reserve(100);
    
    println!("{}",my_vec.capacity());
    println!("{}",my_vec.len());
    
}

Convert Array to Vector

fn main(){
    let arr1 = [1,2,3,4];
    let my_vec:Vec<i8> = arr1.into();
    let my_vec1:Vec<_> = arr1.into();
    
    println!("{:?},{:?},{:?}",arr1,my_vec,my_vec1);
    
    print_type_of(&arr1);
    print_type_of(&my_vec);
    print_type_of(&my_vec1);
}


fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}

Sort the Vector

fn main() {
    let mut vec = vec![14, 33, 12, 56, 3223, 2211, 9122, 3, 299, 67];
    vec.sort();
    println!("Sorted: {:?}", vec)
}

Hash Map

A HashMap is a data structure that provides a highly efficient way to store and retrieve key-value pairs. It's often used when you need to associate unique keys with specific values and want fast access to the values based on their keys.

  • Data type to store data in Key - Value pair.

  • Keys are used to looking up corresponding values.

  • Hashing: The keys are processed through a hash function, which converts the key into an index (or hash code) that determines where the key-value pair is stored in the underlying array.

  • Dynamic Resizing: HashMap dynamically resizes its internal array to maintain performance as the number of elements grows.

  • Non-Sequential: The order of elements in a HashMap is not guaranteed to be the same as the order in which they were inserted. The order is determined by the hash codes of the keys.


Example: Phone book - Search for the name and get the phone number

Under the hood, a hash function determines how to store data so the value can be quickly located.

In other languages, we have a similar feature.

Map / Dictionary / Associative Array

Rules

  • All values must have the same data type.
  • Each key can only have one value associated with it at a time.
  • No duplicate keys.

What is Hashing??

Hashing is a process of transforming any given input (such as a string or a file) into a fixed-size string of characters, which is typically a sequence of numbers and letters.

Fixed-Size Output: No matter the size of the input, the output (called the hash value) will always have a fixed size.

Uniqueness: Ideally, different inputs should produce different hash values, although collisions (where two different inputs produce the same hash value) can happen.

Efficiency: Hashing is designed to be fast and efficient, making it useful for quick data retrieval.

Hasing Example

use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

fn main() {
    let s = "hello world";

    // Create a hasher
    let mut hasher = DefaultHasher::new();

    // Hash the string
    s.hash(&mut hasher);

    // Get the resulting hash as a u64
    let hash_value = hasher.finish();

    println!("Hash value for '{}': {}", s, hash_value);
}

HashMap Example

use std::collections::HashMap;

fn main() {
    let mut cities = HashMap::new();
    cities.insert("Glassboro", 30000);
    cities.insert("Mullicahill", 25000);
    cities.insert("Swedesboro", 28000);
   
    println!("cities is {:?}", cities);
}

Insert

use std::collections::HashMap;

fn main() {
    let mut cities = HashMap::new();
    cities.insert("Glassboro", 30000);
    cities.insert("Mullicahill", 25000);
    cities.insert("Swedesboro", 28000);
   
    println!("cities is {:?}", cities);

    //Option 1 - Insert / Overwrite Existing Value

    cities.insert("Paulsboro", 11000);
    cities.insert("Glassboro", 31000);

	println!("cities is {:?}", cities);
}

Insert/Update/Get

use std::collections::HashMap;

fn main() {
    let mut cities = HashMap::new();
    cities.insert("Glassboro", 30000);
    cities.insert("Mullicahill", 25000);
    cities.insert("Swedesboro", 28000);
    
    //Option 1 - Update / Overwrite Existing Value
    cities.insert("Glassboro", 31000);
    
    //Option 2 - Insert a new entry if it doesn't exist
    cities.entry("Depford").or_insert(12000);
    
    //Option 3 - Get the value of the Key and perform a mathematical operation
    let gpopulation = cities.entry("Glassboro").or_insert(0);
    *gpopulation += 1;
    
    //print the hash map values. The print order may or may not be the same
    //as the insert. It changes from time to time.
    println!("cities is {:?}", cities);

    let glassboro_population = cities.get("Glassboro");
    
    if glassboro_population.is_some(){
        println!("glassboro_population is {:?}", glassboro_population);
    }
    else if glassboro_population.is_none(){
        println!("glassboro_population is not available", );
    }
}

Remove

use std::collections::HashMap;

fn main() {
    let mut cities = HashMap::new();
    cities.insert("Glassboro", 30000);
    cities.insert("Mullicahill", 25000);
    cities.insert("Swedesboro", 28000);

    // Remove an entry
    cities.remove("Mullicahill");

    println!("After removal: {:?}", cities);
}

Iterating Over the HashMap

use std::collections::HashMap;

fn main() {
    let mut cities = HashMap::new();
    cities.insert("Glassboro", 30000);
    cities.insert("Mullicahill", 25000);
    cities.insert("Swedesboro", 28000);

    // Iterating over the HashMap
    for (city, population) in &cities {
        println!("The population of {} is {}", city, population);
    }
}

Check for Existence

use std::collections::HashMap;

fn main() {
    let mut cities = HashMap::new();
    cities.insert("Glassboro", 30000);
    cities.insert("Mullicahill", 25000);
    cities.insert("Swedesboro", 28000);

    // Checking if a key exists
    if cities.contains_key("Swedesboro") {
        println!("Swedesboro is in the HashMap");
    } else {
        println!("Swedesboro is not in the HashMap");
    }
}

Merge

use std::collections::HashMap;

fn main() {
    let mut cities1 = HashMap::new();
    cities1.insert("Glassboro", 30000);
    cities1.insert("Mullicahill", 25000);

    let mut cities2 = HashMap::new();
    cities2.insert("Swedesboro", 28000);
    cities2.insert("Depford", 12000);

    // Merging two HashMaps
    for (city, population) in cities2 {
        cities1.insert(city, population);
    }

    println!("Merged cities: {:?}", cities1);
}

Structs

Popular custom datatype for grouping related values. Like a Tuple, Structs help to group related values of mixed data types.

Unlike a Tuple, you will assign a name to each value to indicate what it means. In the tuple, you will be using .0 .1 notation, but with struct, you can use the actual name.

Classic Struct

- Most commonly used.
- Each field has a name and a type.
struct Students {
	id:i32,
	name:String,
	course:String
}

fn main(){
    let s:Students = Students{
        id:10,
        name:String::from("Rachel"),
        course:String::from("DB")
    };

    println!("{},{},{}",s.id,s.name,s.course);

}

Similar to Type alias, the name of the struct begins with an uppercase letter followed by lowercase characters.

Structs are usually declared outside the main function. If the scope is local, it can be declared inside the main.

Where the data is store Stack or Heap?

Lets find out where the objects inside Struct are created.

#[allow(dead_code)]
struct Students {
    id: i32,
    name: String,
    course: String,
}

fn main() {
    let s: Students = Students {
        id: 10,
        name: String::from("Rachel"),
        course: String::from("DB"),
    };

    println!(
        "id value -> {} \
    \naddress of s.id -> {:p} \
    \nAddress of s -> {:p} \
    \nAddress of s.name -> {:p} \
    \nAddress of s.course -> {:p} \
    \ns.name pointing to -> {:p} \
    \ns.course pointing to -> {:p}",
        s.id,
        &s.id,
        &s,
        &s.name,
        &s.course,
        s.name.as_ptr(),
        s.course.as_ptr()
    );
}
Stack:
---------
| id (10) |
---------
| name (pointer, length, capacity) |
---------
| course (pointer, length, capacity) |
---------

Heap:
--------------------------------
| "Rachel" (string data)        |
--------------------------------
| "DB" (string data)            |
--------------------------------

Struct - Stack or Heap?

// Struct Stack or Heap

struct Students {
    id: i32
}

fn main() {
    let s: Students = Students {
        id: 10
    };

    let s4 = s; //Is this COPY or Move ?
    
    println!("{}",s.id)
    
}
// lets clone it so we can make a copy of the Struct

let s4 = s.clone();

What do we see now?

method `clone` not found for this struct

Let's set the Clone trait for the struct Students

// Adding Clone trait to the struct Students
#[derive(Clone)]
struct Students{id:i32}

Instead of printing s.id can you try printing the entire Struct?

// Using debug trait print the value of S

println!("{:?}",s4);

Now, what do you see?

// Adding Clone and Debug trait to the struct Students

#[derive(Clone,Debug)]
struct Students{id:i32}

Putting together all of the above

// Struct Stack or Heap

#[derive(Clone,Debug)]
struct Students {
    id: i32
}

fn main() {
    let s: Students = Students {
        id: 10
    };

    let s4 = s.clone();
    
    println!("{}",s.id);
    println!("{:?}",s4);
    
}

Struct inside Vector

#[derive(Debug, Clone)]
struct Students {
    id: i32,
    name: String,
    course: String,
}

fn main() {
    let s: Students = Students {
        id: 10,
        name: String::from("Rachel"),
        course: String::from("DB"),
    };

    let s1: Students = Students {
        id: 11,
        name: String::from("Monica"),
        course: String::from("DB"),
    };

    let s2: Students = Students {
        id: 12,
        name: String::from("Phoebe"),
        course: String::from("DB"),
    };

    let mut s_vec: Vec<Students> = Vec::new();

    s_vec.push(s);
    s_vec.push(s1);
    s_vec.push(s2);

    for v in s_vec.iter() {
         println!("{},{},{}", v.id, v.name, v.course);
    }
}

Struct implementation

The implementation block has the keyword "impl" followed by the same name as Struct. It contains methods and functions. A struct can have more than one method or function.

Methods: methods are similar to functions with "fn" keyword. They can have parameters and return values. The only difference is, they are defined within the context of a struct and their first parameter is always self.

In Rust, the self parameter in method signatures refers to the instance of the struct on which the method is called.

Let's see an example

#[derive(Debug, Clone)]
struct Students {
    id: i32,
    name: String,
    course: String,
}

impl Students {
    
    // either use self or self: &Self it all means the same
    //fn get_student_details(self){
    
    fn get_student_details(self: &Self){
        println!("{}", self.id);
        println!("{}", self.name);
        println!("{}", self.course);
    }

}

fn main() {
    let s: Students = Students {
        id: 10,
        name: String::from("Rachel"),
        course: String::from("DB"),
    };
    
    let s1: Students = Students {
        id: 11,
        name: String::from("Monica"),
        course: String::from("DB"),
    };
    
    s.get_student_details();
    s1.get_student_details();
}    

Using Associated Function

Functions inside the impl block that do not take "self" as a parameter.

#[derive(Debug, Clone)]
struct Students {
    id: i32,
    name: String,
    course: String,
}

impl Students {
    fn get_student_details(self){
        println!("{}", "-".repeat(100));
        println!("{}", self.id);
        println!("{}", self.name);
        println!("{}", self.course);
        println!("{}", "-".repeat(100));
    }
    
    fn create_student(pid:i32,pname:String,pcourse:String) -> Students{
        Students{id:pid,name:pname,course:pcourse}
    }
}

fn main() {

    let s: Students = Students {
        id: 10,
        name: String::from("Rachel"),
        course: String::from("DB"),
    };
    
    let s1: Students = Students {
        id: 11,
        name: String::from("Monica"),
        course: String::from("DB"),
    };
    
    s.get_student_details();
    s1.get_student_details();
    
    // Creating Student using Associated Function and printing it via Method
    
    let s2 = Students::create_student(12,"Ross".to_string(),"C++".to_string());
    s2.get_student_details();
}    

Mutable Implementation

// Using Mutable & Borrow operator

#[derive(Debug, Clone)]
struct Students {
    id: i32,
    name: String,
    course: String,
}

impl Students {
    fn get_student_details(&self){
        println!("{}", "-".repeat(100));
        println!("{}", self.id);
        println!("{}", self.name);
        println!("{}", self.course);
        println!("{}", "-".repeat(100));
    }
    
    fn create_student(pid:i32,pname:String,pcourse:String) -> Students{
        Students{id:pid,name:pname,course:pcourse}
    }
    
    fn change_student_details(&mut self, id:i32, new_name:String, new_course:String){
        self.name=new_name;
        self.course=new_course;
    }
}

fn main() {

    let s = Students {
        id: 10,
        name: String::from("Rachel"),
        course: String::from("DB"),
    };
    
    let s1 = Students {
        id: 11,
        name: String::from("Monica"),
        course: String::from("DB"),
    };
    
    s.get_student_details();
    s1.get_student_details();
    
    // Creating Student using Associated Function and printing it via Method
    
    let mut s2 = Students::create_student(12,"Ross".to_string(),"C++".to_string());
    s2.get_student_details();
    
    s2.change_student_details(12,"Ross Geller".to_string(),"CPP".to_string());
    s2.get_student_details();
}    

Tuple Struct

Tuple Structs - Similar to Classic but fields have no names.

// Tuple Struct

struct Coordinates(u32, u32);

fn main() {
    let xy = Coordinates(10, 20);
    //it behaves like Tuples
    println!("Value of the Tuple Struct xy {},{}", xy.0, xy.1);
    
    //Destructuring Tuple Struct

    let Coordinates(a,b) = xy;
    println!("Values of variables a & b {},{}", a, b);
}

Enums

Enumerators

Define a data type with multiple possible variants.

If Today is Tuesday, it can be written as Tue, Tuesday, TUESDAY, T in several forms. How to standarize it?

Example:

  • Days of the Week.
  • Months in a year.
  • Traffic light colors.

It Enumerates a finite number of options or types.

  • Create custom enum types.
  • How enums are commonly used.
  • There are few standard enums you will use in Rust.
#[derive(Debug)]
enum TrafficLight{
    Red,
    Yellow,
    Green
}

fn main(){
    let my_light = TrafficLight::Red;

    println!("{:?}", my_light);
}

Importance of using derive(Debug) Trait. Execute this script to see the result.

enum TrafficLight{
    Red,
    Yellow,
    Green
}

fn main(){
    let my_light = TrafficLight::Red;

    println!("{:?}", my_light);
}

In addition to simply representing one of several types, we can have additional data based on the value.

Let's add Unnamed parameters in parenthesis.

// Enum with additional data

#[derive(Debug)]
enum TrafficLight{
    Red (bool),
    Yellow (bool),
    Green (bool)
}

fn main(){
    let my_light = TrafficLight::Red(true);

    println!("{:?}", my_light);
}
// Separate the value and enum result

#[derive(Debug)]
enum TrafficLight{
    Red (bool),
    Yellow (bool),
    Green (bool)
}

fn main(){
    let my_light = TrafficLight::Red(false);

    println!("{:?}", my_light);
    
    match my_light {
        TrafficLight::Red(is_active) => println!("Red: {}", is_active),
        TrafficLight::Yellow(is_active) => println!("Yellow: {}", is_active),
        TrafficLight::Green(is_active) => println!("Green: {}", is_active),
    }
}

Enums inside Enums

#[derive(Debug)]
enum TrafficLight {
    Red(bool),
    Yellow(bool),
    Green(bool),
}

#[derive(Debug)]
enum Vehicle {
    Stop,
    Drive(f64),
    CheckLight(TrafficLight),
}

fn main() {
    let my_light = TrafficLight::Red(true);
    let instruction = Vehicle::CheckLight(my_light);

    match instruction {
        Vehicle::Stop => println!("Stop"),
        Vehicle::Drive(speed) => println!("Drive at speed: {}", speed),
        Vehicle::CheckLight(light) => match light {
            TrafficLight::Red(is_active) => println!("Red light: {}", is_active),
            TrafficLight::Yellow(is_active) => println!("Yellow light: {}", is_active),
            TrafficLight::Green(is_active) => println!("Green light: {}", is_active),
        },
    }
}

In Database world, commonly usages are

enum RightClick {
    Copy,
    Paste,
    Save,
    Quit
}

enum FileFormat {
    CSV,
    Parquet,
    Avro,
    JSON,
    XML,
}

enum LogLevel {
    Debug,
    Info,
    Warn,
    Error,
    Critical,
}


enum DataTier {
    Hot,
    Warm,
    Cold,
    Archived,
}

Enum Implements

Similar to Struct, you can implement an interface in Enum.

The Implement interface can be helpful when we need to implement some business logic tightly coupled with a discriminatory property of a given enum.

// Modified from Source: Barron Stone Git Repository

#[derive(Debug)]
enum Shape {
    Circle(f64), // radius
    Rectangle(f64, f64), // width, height
    Triangle(f64, f64, f64) // sides a, b, c
}

impl Shape {
    fn get_perimeter(&self) -> f64 {
        match *self {
            Shape::Circle(r) => r * 2.0 * std::f64::consts::PI,
            Shape::Rectangle(w, h) => (2.0 * w) + (2.0 * h),
            Shape::Triangle(a, b, c) => a + b + c
        }
    }
}

fn main() {
    let my_shape = Shape::Circle(1.2);
    println!("my_shape is {:?}", my_shape);

    let perimeter = my_shape.get_perimeter();
    println!("perimeter is {}", perimeter);
    
    let my_shape1 = Shape::Triangle(3.0,4.0,5.0);
    println!("my_shape is {:?}", my_shape1);

    let perimeter1 = my_shape1.get_perimeter();
    println!("perimeter is {}", perimeter1);
    
    
    let my_shape2 = Shape::Rectangle(4.0,5.0);
    println!("my_shape is {:?}", my_shape2);

    let perimeter2 = my_shape2.get_perimeter();
    println!("perimeter is {}", perimeter2);
    
}

Commonly used standard Enums

Option

Many languages use NULL to indicate no value.

Errors often occur when using a NULL.

Rust does not have a traditional null value.

// Sample Option Enum

fn main() {
    let x = Some(5);  //Option <i32>
    let y = Some(4.0); // Option <f64>
    let z = Some("Hello"); //Option <&str>
    let a = None; //N should be upper case
    let a_vec: Option<Vec<i32>> = Some(vec![0, 1, 2, 3]);
}
// Simple Division

fn try_division(dividend: i32, divisor: i32) -> i32 {
    dividend / divisor
}

fn main() {
    println!("{}",try_division(4, 2));
    //println!("{}",try_division(4, 0));
}

Sometimes it's desirable to catch the failure of some parts of a program instead of calling panic!; this can be accomplished using the Option enum.

// Common Enum

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

This achieves the same concept as a traditional null value, but implementing it in terms of an enum data type compiler can check to make sure you are handling it correctly.

It's commonly used and included in the prelude. That means additional use statements are needed.

// Error Handling

// An integer division that doesn't `panic!`
fn checked_division(dividend: i32, divisor: i32) -> Option<i32> {
    if divisor == 0 {
        // Failure is represented as the `None` variant
        None
    } else {
        // Result is wrapped in a `Some` variant
        Some(dividend / divisor)
    }
}

fn try_division(dividend: i32, divisor: i32) {
    // `Option` values can be pattern matched, just like other enums
    match checked_division(dividend, divisor) {
        None => println!("{} / {} failed!", dividend, divisor),
        Some(quotient) => {
            println!("{} / {} = {}", dividend, divisor, quotient)
        },
    }
}

fn main() {
    //try_division(4, 2);
    try_division(4, 0);
    
}

Result

// Another Prelude

enum Result<T, E>{
  Ok(T),
  Err(E)
}

Ok(T)

It contains the success value.

Err(E)

Contains the error value

// Simple Simple Interest Function

fn si(p:f32,n:f32,r:f32) -> f32 {
	(p * n * r )/100 as f32
}


fn main(){
	let p:f32 = 10000.00;
	let n:f32 = 3.00;
	let r:f32 = 1.4;

	let si = si(p,n,r);
	println!("Simple Interest = {si}");
}

How to handle if the parameter is Zero?

// SI with conditions

fn si(p:f32,n:f32,r:f32) -> f32 {
    if p <= 0. {
        println!("p cannot be zero");
    }
    
    if n <= 0.{
        println!("n cannot be zero");
    }
    
    if r <= 0. {
        println!("r cannot be zero");
    }
    
	(p * n * r )/100 as f32
}


fn main(){
	let p:f32 = 10000.00;
	let n:f32 = 3.00;
	let r:f32 = 0.0;

	let si = si(p,n,r);
	println!("Simple Interest = {si}");
}

Do you notice any issues with this?

Yes, the message is displayed, but still, the calculation takes place, and the Error message is only for informational purposes.

How does Result enum help in this case?

// using Result

fn si(p: f32, n: f32, r: f32) -> Result<f32, String> {
    if p <= 0. {
        return Err("Principal cannot be less or equal to zero".to_string());
    }

    if n <= 0. {
        return Err("Number of years cannot be less or equal to zero".to_string());
    }

    if r <= 0. {
        return Err("Rate cannot be less or equal to zero".to_string());
    }

    Ok((p * n * r) / 100.0)
}

fn main() {
    let p: f32 = 10000.0;
    let n: f32 = 3.0;
    let r: f32 = 1.4;

    match si(p, n, r) {
        Ok(result) => println!("si = {result}"),
        Err(e) => println!("error occured {:?}", e),
    }
}

Difference when using Result and Option Example

// using Result

fn si_using_result(p: f32, n: f32, r: f32) -> Result<f32, String> {
    if p <= 0. {
        return Err("Principal cannot be less or equal to zero".to_string());
    }

    if n <= 0. {
        return Err("Number of years cannot be less or equal to zero".to_string());
    }

    if r <= 0. {
        return Err("Rate cannot be less or equal to zero".to_string());
    }

    Ok((p * n * r) / 100.0)
}

fn si_using_option(p: f32, n: f32, r: f32) -> Option<f32> {
    if p <= 0. {
        return None;
    }

    if n <= 0. {
        return None;
    }

    if r <= 0. {
        return None;
    }

    Some((p * n * r) / 100.0)
}

fn main() {
    let p: f32 = 10000.0;
    let n: f32 = 0.0;
    let r: f32 = 1.4;

    match si_using_result(p, n, r) {
        Ok(result) => println!("si = {result}"),
        Err(e) => println!("Result ENUM - Error occured {:?}", e),
    }
    
    match si_using_option(p, n, r) {
        Some(result) => println!("si = {result}"),
        None => println!("Option ENUM - Error occurred: one of the inputs is non-positive"),
    }
}

The ? operator

? is used for Error Propogation

There is an even shorter way to deal with Result (and Option), shorter than a match and even shorter than if let. It is called the "question mark operator."

After a function that returns a result, you can add ?. This will:

If its Ok, return the result If its Err, return the error

Before using ?

fn si_using_result(p: f32, n: f32, r: f32) -> Result<f32, String> {
    if p <= 0. {
        return Err("Principal cannot be less or equal to zero".to_string());
    }

    if n <= 0. {
        return Err("Number of years cannot be less or equal to zero".to_string());
    }

    if r <= 0. {
        return Err("Rate cannot be less or equal to zero".to_string());
    }

    Ok((p * n * r) / 100.0)
}

fn main() {
    let p: f32 = 10000.0;
    let n: f32 = 0.0;
    let r: f32 = 1.4;

    match si_using_result(p, n, r) {
        Ok(result) => println!("si = {result}"),
        Err(e) => println!("Result ENUM - Error occured {:?}", e),
    }

}

Now using ?

Returning Ok()

fn si_using_result(p: f32, n: f32, r: f32) -> Result<f32, String> {
    if p <= 0. {
        return Err("Principal cannot be less or equal to zero".to_string());
    }

    if n <= 0. {
        return Err("Number of years cannot be less or equal to zero".to_string());
    }

    if r <= 0. {
        return Err("Rate cannot be less or equal to zero".to_string());
    }

    Ok((p * n * r) / 100.0)
}

fn main() -> Result<(), String> {
    let p: f32 = 10000.0;
    let n: f32 = 3.0;
    let r: f32 = 1.4;

    let result = si_using_result(p, n, r)?;
    println!("SI calc with {p},{n},{r} = {result}");

    Ok(())
}

Returning Err

fn si_using_result(p: f32, n: f32, r: f32) -> Result<f32, String> {
    if p <= 0. {
        return Err("Principal cannot be less or equal to zero".to_string());
    }

    if n <= 0. {
        return Err("Number of years cannot be less or equal to zero".to_string());
    }

    if r <= 0. {
        return Err("Rate cannot be less or equal to zero".to_string());
    }

    Ok((p * n * r) / 100.0)
}

fn main() -> Result<(), String> {
    let p: f32 = 10000.0;
    let n: f32 = 0.0;
    let r: f32 = 1.4;

    let result = si_using_result(p, n, r)?;
    println!("SI calc with {p},{n},{r} = {result}");

    Ok(())
}

Another way to write using a intermediate function

fn calc_si(p: f32, n: f32, r: f32) -> Result<f32, String> {
    if p <= 0. {
        return Err("Principal cannot be less or equal to zero".to_string());
    }

    if n <= 0. {
        return Err("Number of years cannot be less or equal to zero".to_string());
    }

    if r <= 0. {
        return Err("Rate cannot be less or equal to zero".to_string());
    }

    Ok((p * n * r) / 100.0)
}

fn print_si(p: f32, n: f32, r: f32) -> Result<f32, String>{
    let result_si = calc_si(p,n,r)?;
    Ok(result_si)
}

fn main() {
    let p: f32 = 10000.0;
    let n: f32 = 3.;
    let r: f32 = 1.4;

    println!("{:?}",print_si(p,n,r));

}

Loop through and convert Integers and return Error message for others

// Simple Parse Example

fn parse_str(input: &str) -> Result<i32, std::num::ParseIntError> {
    let parsed_number = input.parse::<i32>()?;
    Ok(parsed_number)
}

fn main() {
    let str_vec = vec!["Seven", "8", "9.0", "nice", "6060"];
    for item in str_vec {
        let parsed = parse_str(item);
        println!("{:?}", parsed);
    }
}

unwrap is a method available on Option and Result types that is used to extract the contained value. If the Option is Some or the Result is Ok, unwrap returns the contained value. However, if the Option is None or the Result is Err, unwrap will cause the program to panic and terminate execution.

• Use unwrap sparingly and only when you are sure it won’t cause a panic. • Prefer expect with a meaningful message for clarity in cases where you are sure the operation won’t fail.


fn parse_str(input: &str) -> Result<i32, std::num::ParseIntError> {
    let parsed_number = input.parse::<i32>()?;
    Ok(parsed_number)
}

fn main() {
    let str_vec = vec!["Seven", "8", "9.0", "nice", "6060"];
    for item in str_vec {
        let parsed = parse_str(item).unwrap();
        println!("{:?}", parsed);
    }
}

unwrap_or_else

This can be used in production code.

fn parse_str(input: &str) -> Result<i32, std::num::ParseIntError> {
    let parsed_number = input.parse::<i32>()?;
    Ok(parsed_number)
}

fn main() {
    let str_vec = vec!["Seven", "8", "9.0", "nice", "6060"];
    for item in str_vec {
        let parsed = parse_str(item).unwrap_or_else(|err| {
            println!("Error: failed to parse '{}'. Reason: {}", item, err);
            -1 // Provide a fallback value
        });
        if parsed != -1 {
            println!("{}", parsed);
        }
    }
}

Input from user

// Input String

use std::io;

fn main(){
    println!("Please enter your name: ");

    let mut name = String::new();
    io::stdin().read_line(&mut name).expect("Failed");

    println!("Welcome {}",name);
}

Expect (): Is used when the program panics.

// Accept two numbers

use std::io;

fn main(){
    println!("Enter First Number: ");

    let mut s1 = String::new();
    io::stdin().read_line(&mut s1).expect("Not a valid input");
    let n1:u32 = s1.trim().parse().expect("Not a valid Number");

    
    println!("Enter Second Number: ");
    let mut s2 = String::new();
    io::stdin().read_line(&mut s2).expect("Not a valid input");
    let n2:u32 = s2.trim().parse().expect("Not a valid Number");


    let result = n1 + n2;

    println!("Result : {result}");
}

Vector - Struct - Input

use std::io;

#[derive(Debug)]
struct User {
    id: String,
    first_name: String,
    last_name: String,
    status: String,
}

fn main() {
    let mut users: Vec<User> = Vec::new();

    loop {
        println!("Enter 'id', 'first name', 'last name', 'status' separated by SPAC:");
        let mut input = String::new();
        io::stdin().read_line(&mut input).unwrap();
        let parts: Vec<&str> = input.trim().split_whitespace().collect();

        if parts.len() != 4 {
            println!("Invalid input. Please enter 4 values.");
            continue;
        }

        let new_user = User {
            id: parts[0].to_string(),
            first_name: parts[1].to_string(),
            last_name: parts[2].to_string(),
            status: parts[3].to_string(),
        };

        users.push(new_user);

        println!("Do you want to add another user? (Yes/No/Y/N): ");
        let mut continue_input = String::new();
        io::stdin().read_line(&mut continue_input).unwrap();

        if continue_input.trim().eq_ignore_ascii_case("n") || continue_input.trim().eq_ignore_ascii_case("no")  {
            break;
        }
    }

    println!("\nAll users:");
    for user in users {
        println!("{},{},{},{}", user.id, user.first_name, user.last_name, user.status);
    }
}

Command Line Args

Command Line Arguments

CLI Arguments are passed to the program when it is invoked.

Example

rustc --version

Commonly used for File Paths & Configuration Settings.

To use Command Line Arguments, you need to include a standard libraries environment module.

std::env::args

Returns an iterator over arguments passed to the program.

The first argument is traditionally the executable path.

// command line arguments

use std::env;

fn main(){
    for (index,argument) in  env::args().enumerate(){
        println!("{},{}", index, argument)
    }
}

Pick a specific argument.

// option 1

let arg2 = eng::args().nth(2);
prinln!("{}", arg2);

// another option
let name = env::args().skip(1).next();

// dbg macro
dbg!(env::args());

Some

use std::env;

fn main() {
    println!("{:?}",env::args());
    
    let name = env::args().skip(1).next();
    
    match name{
        Some(n) => println!("Hi {n}"),
        None => panic!("Missing parameter")
    }
   
}

Checking for the number of arguments.

Example for copying you need src and destination


if env::args().len() <= 2{
    println!("need atleast 2 args");
    return;  //exists the program
}

Modules

Modules Overview

The Powerful module system can split the code into hierarchical logical units.

The module is a collection of items: functions, structs, and even other modules.

Standard Modules

Like python/java/c/c++ Rust has standard modules which can be used for standard functionality.

Example:

The std::io the module contains a number of common things you’ll need when doing input and output.

Similarly when working with Files

use std::fs::File;

can be used.

Refer to this page for a list of modules

https://doc.rust-lang.org/std/io/index.html#modules

User Defined Module

The Powerful module system can split the code into hierarchical logical units.

The module is a collection of items: functions, structs, and even other modules.

By default, items in a module are private; they can be changed to the public by adding pub before it.

Simple example

// Mod

mod sales{
    pub fn meet_customer(){
        println!("meet customer"); 
    }
       
}

fn main(){
    sales::meet_customer();
   
}

Pass params

    mod sales {
        pub fn meet_customer(num:i32) {
            println!("meet customer {num}");
        }
    }


fn main() {
    sales::meet_customer(1);
}

Companies have multiple depts so nesting the modules help in the hierarchy.

Note Sales module has to pub.


mod departments {
    pub mod sales {
        pub fn meet_customer(num:i32) {
            println!("meet customer {num}");
        }

    }
}

fn main() {
    departments::sales::meet_customer(1);
}

Exposing only limited functionality

// Here meet_customer calls get_number as that function is not 
// not exposed to main function

mod departments {
    pub mod sales {
        pub fn meet_customer(num:i32,requestedby:&str) {
            println!("meet customer {num}");
            let phone_number = get_number(num, requestedby);
            println!("calling {:?}", phone_number);
        }

        fn get_number(num:i32,requestedby:&str) -> String {
            println!("{requestedby}");
            let phonenumber = match num {
                1 => "123-456-7890".to_string(),
                2 => "987-654-3210".to_string(),
                _ => "000-000-0000".to_string()
            };
            
            if requestedby == "Manager"{
                return phonenumber
            }
            else if requestedby == "CustService"
            {
                return phonenumber[8..].to_string()
            }
            else{
                return "".to_string()
            }
        }
    }
}

fn main() {
    departments::sales::meet_customer(1,"Manager");
    departments::sales::meet_customer(1,"CustService");
}

Invoking the parent private function using super::

// super::

mod departments {
    fn get_number(num:i32) -> String {
        match num {
            1 => return "123-456-7890".to_string(),
            2 => return "987-654-3210".to_string(),
            _ => return "000-000-0000".to_string()
        }
    }


    pub mod sales {
        pub fn meet_customer(num:i32) {
            println!("Sales : meet customer {num}");
            let phone_number = super::get_number(num);
            println!("Sales calling {}", phone_number);
        }
    }

    pub mod service {
        pub fn meet_customer(num:i32) {
            println!("Service : meet customer {num}");
            let phone_number = super::get_number(num);
            println!("Service calling {}", phone_number);
        }
    }
}

fn main() {
    departments::sales::meet_customer(1);
    departments::service::meet_customer(3);
}

Example for self::

// self::

mod departments {
    fn get_number(num:i32) -> String {
        match num {
            1 => return "123-456-7890".to_string(),
            2 => return "987-654-3210".to_string(),
            _ => return "000-000-0000".to_string()
        }
    }


    pub mod sales {
        pub fn meet_customer(num:i32) {
            println!("Sales : meet customer {num}");
            let phone_number = super::get_number(num);
            println!("Sales calling {}", phone_number);
        }
    }

    pub mod service {
        pub fn meet_customer(num:i32) {
            println!("Service : meet customer {num}");
            let phone_number = super::get_number(num);
            let ticket_number = self::get_service_ticket_number(num);
            println!("Calling {phone_number} with ticket number {ticket_number}");
        }

        fn get_service_ticket_number(num:i32)->i32{
            match num {
                1 => return 2452423,
                2 => return 2341332,
                _ => return 6868765
            } 
        }
    }
}

fn main() {
    departments::sales::meet_customer(1);
    departments::service::meet_customer(3);
}


Putting it all together along with TEST Cases

// With Test Cases

mod departments {
    fn get_number(num: i32) -> String {
        match num {
            1 => return "123-456-7890".to_string(),
            2 => return "987-654-3210".to_string(),
            _ => return "000-000-0000".to_string(),
        }
    }

    pub mod sales {
        pub fn meet_customer(num: i32) {
            println!("Sales : meet customer {num}");
            let phone_number = super::get_number(num);
            println!("Sales calling {}", phone_number);
        }
    }

    pub mod service {
        pub fn meet_customer(num: i32) {
            println!("Service : meet customer {num}");
            let phone_number = super::get_number(num);
            let ticket_number = self::get_service_ticket_number(num);
            println!("Calling {phone_number} with ticket number {ticket_number}");
        }

        fn get_service_ticket_number(num: i32) -> i32 {
            match num {
                1 => return 2452423,
                2 => return 2341332,
                _ => return 6868765,
            }
        }
    }

    #[cfg(test)] // Only compiles when running tests
    mod tests {
        use crate::get_standard_greetings;

        #[test]
        fn test_customerphone() {
            assert_eq!("000-000-0000", super::get_number(4));
        }

        #[test]
        fn test_standard_greeting() {
            assert_eq!("Welcome to our store.", get_standard_greetings());
        }
    }
}

fn main() {
    println!("{:?}", get_standard_greetings());
    departments::sales::meet_customer(1);
    departments::service::meet_customer(3);
}

fn get_standard_greetings() -> String {
    return "Welcome to our store.".to_string();
}

Module Multiple files

cargo new moddemo2

create files as given below

// departments.rs

pub mod dept {
    fn get_number(num: i32) -> String {
        match num {
            1 => return "123-456-7890".to_string(),
            2 => return "987-654-3210".to_string(),
            _ => return "000-000-0000".to_string(),
        }
    }

    pub mod sales {
        pub fn meet_customer(num: i32) {
            println!("Sales : meet customer {num}");
            let phone_number = super::get_number(num);
            println!("Sales calling {}", phone_number);
        }
    }

    pub mod service {
        pub fn meet_customer(num: i32) {
            println!("Service : meet customer {num}");
            let phone_number = super::get_number(num);
            let ticket_number = self::get_service_ticket_number(num);
            println!("Calling {phone_number} with ticket number {ticket_number}");
        }

        fn get_service_ticket_number(num: i32) -> i32 {
            match num {
                1 => return 2452423,
                2 => return 2341332,
                _ => return 6868765,
            }
        }
    }

    #[cfg(test)] // Only compiles when running tests
    mod tests {
        use crate::get_standard_greetings;

        #[test]
        fn test_customerphone() {
            assert_eq!("000-000-0000", super::get_number(4));
        }

        #[test]
        fn test_standard_greeting() {
            assert_eq!("Welcome to our store.", get_standard_greetings());
        }
    }
}

// main.rs


// Refer the external file
mod departments;

// Import the module
use departments::dept;

fn main() {
    println!("{:?}", get_standard_greetings());
    dept::sales::meet_customer(1);
    dept::service::meet_customer(3);
}

fn get_standard_greetings() -> String {
    return "Welcome to our store.".to_string();
}

Module Sub Folders

This way of organizing works for major projects.

The code in mod.rs is the content of that module. All other files in the folder may in turn be exposed as submodules.

cargo new moddemo3

create the following directory structure

// main.rs

mod departments;

fn main() {
    println!("{:?}", get_standard_greetings());
    departments::sales::meet_customer(1);
    departments::service::meet_customer(3);
}

fn get_standard_greetings() -> String {
    return "Welcome to our store.".to_string();
}
// departments > mod.rs

fn get_number(num: i32) -> String {
    match num {
        1 => return "123-456-7890".to_string(),
        2 => return "987-654-3210".to_string(),
        _ => return "000-000-0000".to_string(),
    }
}

pub mod sales;
pub mod service;
pub mod tests;
// departments > sales.rs

pub fn meet_customer(num: i32) {
    println!("Sales : meet customer {num}");
    let phone_number = super::get_number(num);
    println!("Sales calling {}", phone_number);
}

// departments > service.rs

pub fn meet_customer(num: i32) {
    println!("Service : meet customer {num}");
    let phone_number = super::get_number(num);
    let ticket_number = self::get_service_ticket_number(num);
    println!("Calling {phone_number} with ticket number {ticket_number}");
}

fn get_service_ticket_number(num: i32) -> i32 {
    match num {
        1 => return 2452423,
        2 => return 2341332,
        _ => return 6868765,
    }
}
// departments > tests.rs

#[cfg(test)] // Only compiles when running tests
use crate::get_standard_greetings;

#[test]
fn test_customerphone() {
    assert_eq!("000-000-0000", super::get_number(4));
}

#[test]
fn test_standard_greeting() {
    assert_eq!("Welcome to our store.", get_standard_greetings());
}

Read more

{% embed url="https://spin.atomicobject.com/2022/01/24/rust-module-system/" %}

Creating a Library

Creating Binary (executable) files is one option; another option is to create your Library files.

lib.rs - is used to create a library crate.

Part 1: Create a Department library

To create a new library include --lib when creating a new cargo package

cargo new newlib --lib

#![allow(unused)]
fn main() {
// departments.rs

pub mod dept {
    fn get_number(num: i32) -> String {
        match num {
            1 => return "123-456-7890".to_string(),
            2 => return "987-654-3210".to_string(),
            _ => return "000-000-0000".to_string(),
        }
    }

    pub mod sales {
        pub fn meet_customer(num: i32) {
            println!("Sales : meet customer {num}");
            let phone_number = super::get_number(num);
            println!("Sales calling {}", phone_number);
        }
    }

    pub mod service {
        pub fn meet_customer(num: i32) {
            println!("Service : meet customer {num}");
            let phone_number = super::get_number(num);
            let ticket_number = self::get_service_ticket_number(num);
            println!("Calling {phone_number} with ticket number {ticket_number}");
        }

        fn get_service_ticket_number(num: i32) -> i32 {
            match num {
                1 => return 2452423,
                2 => return 2341332,
                _ => return 6868765,
            }
        }
    } 
}

}
// lib.rs

pub mod departments;
// cargo build

Part 2: Use the above library

// from terminal

cargo new newlib-test
// main.rs

use newlib::departments::dept;

fn main() {
    dept::sales::meet_customer(1);
    dept::service::meet_customer(3);
}
// cargo.toml

[dependencies]
newlib = {path = "../newlib"}

Chapter 5

File Handling

FS Module is not part of the default prelude.

Rust Prelude

The prelude is the list of things that Rust automatically imports into every Rust program. It’s kept as small as possible and is focused on things, particularly traits, which are used in almost every single Rust program.

The Data file should be in the same level src folder.

data.txt should be at the same level src folder

#![allow(unused)]
fn main() {
// data.txt

Rachel
Monica
Phoebe
Chandler
Joey
Ross
}

Read File

// main.rs
use std::fs;

fn main(){
	let contents = fs::read_to_string("data.txt").unwrap();
	println!("{:?}",contents);
}

Read Line by Line

use std::fs;

fn main(){
	let contents = fs::read_to_string("data.txt").unwrap();
	for line in contents.lines(){
		println!("{}",line);
	}
}

Rust can also read nontext files (such as images and binaries)

It reads as a vector of u8.

// Read as vector of u8

use std::fs;

fn main(){
	let contents = fs::read("data.txt").unwrap();
	println!("{:?}", contents);
}

Write File

// Write a simple file

use std::fs;

fn main() {
    let mut text = String::new();
    text.push_str("Rust is strong and statically typed language.");
    text.push_str("Rust is super strict.");
   
    fs::write("newfile.txt",text);
}

Points to remember

Simple to use

Will replace the contents of an existing file

Writes entire contents of the file.

How to append

use std::fs;
use std::io::prelude::*;

fn main() {
    let mut text = String::new();
    text.push_str("Rust is strong and statically typed language.");
    text.push_str("Rust is super strict.");

    //fs::write("newfile.txt",text);

    let mut file = fs::OpenOptions::new().append(true).open("newfile.txt").unwrap();
    file.write(b"Rust is awesome");
}

Write fn doesn't care about datatype. It thinks data is a series of bytes and it expects the value to be an array of u8 values.

Simple Find Command Line simulator

#![allow(unused)]
fn main() {
// friends.txt

Rachel
Monica
Phoebe
Chandler
Joey
Ross
}
//cargo run friends.txt Ross

use std::env;
use std::fs;

fn main() {
    if env::args().len() < 2 {
        eprintln!("Program requires two arguments: <file path> <search name>");
        std::process::exit(1);
    }
    let file_path = env::args().nth(1).unwrap();
    let search_name = env::args().nth(2).unwrap();

    let contents = fs::read_to_string(file_path).unwrap();
    
    for line in contents.lines() {
        if line == search_name {
            println!("{} is part of Friends show", search_name);
            return;
        }
    }

    println!("{} is not a part of Friends show!", search_name);
}

Good resource if you want to recreate standard Linux commands using RUST

https://doc.rust-lang.org/rust-by-example/std_misc/fs.html

You will come across few new notations like OK, Err we will read about them in the coming weeks.

JSON

The flexible way to store & share data across systems. It's a text file with curly braces & key-value pairs { }

Simplest JSON format

{"id": "1","name":"Rachel"}

Properties

Language Independent.

Self-describing and easy to understand.

Basic Rules

Curly braces to hold the objects.

Data is represented in Key-Value or Name-Value pairs.

Data is separated by a comma.

The use of double quotes is necessary.

Square brackets [ ] hold an array of data.

JSON Values

String  {"name":"Rachel"}

Number  {"id":101}

Boolean {"result":true, "status":false}  (lowercase)

Object  {
            "character":{"fname":"Rachel","lname":"Green"}
        }

Array   {
            "characters":["Rachel","Ross","Joey","Chanlder"]
        }

NULL    {"id":null}

Sample JSON Document

{
    "characters": [
        {
            "id" : 1,
            "fName":"Rachel",
            "lName":"Green",
            "status":true
        },
        {
            "id" : 2,
            "fName":"Ross",
            "lName":"Geller",
            "status":true
        },
        {
            "id" : 3,
            "fName":"Chandler",
            "lName":"Bing",
            "status":true
        },
        {
            "id" : 4,
            "fName":"Phebe",
            "lName":"Buffay",
            "status":false
        }
    ]
}

JSON Best Practices

No Hyphen in your Keys.

{"first-name":"Rachel","last-name":"Green"}  is not right. ✘

Under Scores Okay

{"first_name":"Rachel","last_name":"Green"} is okay ✓

Lowercase Okay

{"firstname":"Rachel","lastname":"Green"} is okay ✓

Camelcase best

{"firstName":"Rachel","lastName":"Green"} is the best. ✓

JSON & RUST

  • The Deserialize trait is required to parse (that is, read) JSON strings into this Struct.
  • The Serialize trait is required to format (that is, write) this Struct into a JSON string.
  • The Debug trait is for printing a Struct on a debug trace.
// main.rs

use serde_derive::{Deserialize, Serialize};
use std::env;
use std::fs;

// Remember attributes should be below the use statements

#[allow(non_snake_case)]

#[derive(Deserialize, Serialize, Debug)]
struct Characters {
    id: u32,
    fName: String,
    lName: String,
    status: bool
}

#[derive(Deserialize, Serialize, Debug)]
struct CharacterArray {
    characters: Vec<Characters>
}

fn main()  {
    let input_path = env::args().nth(1).unwrap();
    //let output_path = env::args().nth(2).unwrap();

    let friends = {
        let jsondata = fs::read_to_string(&input_path).unwrap();
        // Load the Friends structure from the string.
        serde_json::from_str::<CharacterArray>(&jsondata).unwrap()
    };
    
    for index in 0..friends.characters.len() {
        println!("{} - {}",friends.characters[index].fName,friends.characters[index].lName);
    }
}

Save the above json document as sample.json

// cargo.toml

[dependencies]
serde = "1.0.147"
serde_derive = "1.0.147"
serde_json = "1.0.87"

Convert Struct to JSON

use serde_derive::{Serialize};

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]

struct Person {
    first_name: String,
    last_name: String,
    id: i32,
    status: bool
}

fn main() {
    let person = Person {
        first_name: "Rachel".to_string(),
        last_name: "Green".to_string(),
        id: 1,
        status:true
    };

    let person1 = Person {
        first_name: "Monica".to_string(),
        last_name: "Geller".to_string(),
        id: 2,
        status:true
    };
    
    let v = vec![&person,&person1];

    let output_path = "sample_output.json";

    let json = serde_json::to_string_pretty(&v).unwrap();  // <- unwrap

    println!("{}", json);
    
    std::fs::write(
        output_path,
        json,
    ).unwrap()
}
// cargo.toml

serde = "1.0.147"
serde_derive = "1.0.147"
serde_json = "1.0.87"

Macros

Rust provides a powerful macro system that allows metaprogramming. As you've seen in previous chapters, macros look like functions, except that their name ends with a bang !, but instead of generating a function call, macros are expanded into source code that gets compiled with the rest of the program.

Macros are created using the macro_rules! Macro.

() -- takes no argument ($argument: designator)

Popular designators are

expr is used for expressions

ty is used to type

ident is used for variable/function names

Syntax

macro_rules! <name of macro>{
    () => {}
}
() - Match for the pattern
{} - Expand the code / Body of the macro
// Macro with No argument

macro_rules! print_hello {
    () => {
        // The macro will expand into the contents of this block.
        println!("Hello World");
    };
}

fn main() {
    print_hello!();
}

Macro returning a constant value

// Macro returning value 10

macro_rules! ten {
    () => {5 + 5};
}

fn main(){
    println!("{}",ten!());
}

Macro with one argument

using expr designator

// Macro with one argument

macro_rules! hi {
    ($name:expr) => {
        println!("Hello {}!",$name);
    };
}

fn main() {
    hi!("Rachel");
}

Simple addition macro

// Takes two arguments

macro_rules! add{
    ($a:expr,$b:expr)=>{
         {
            $a+$b
        }
    }
}

fn main(){
    let c = add!(1,2);
    println!("{c}");
}

Demo Stringify

// Stringify
//In Rust, stringify! is a macro that takes a Rust expression 
//and converts it into a string literal 

fn main() {
    println!("{},{}",1+1,stringify!("1+1"));
}

Macro with Expressions

// More Expressions

macro_rules! print_result {
    ($expression:expr) => {
        println!("{:?} = {:?}", stringify!($expression), $expression);
    };
}

fn main() {
    print_result!(1 + 1);

    // Recall that blocks are expressions too!
    print_result!({
        let x = 10; x * x + 2 * x - 1
    });
}

using expr and ty designators

// multiple designators
// variables with different datatypes can be added using this macro

macro_rules! add_using{
    // using a ty token type for matching datatypes passed to the macro
    
    ($x:expr,$y:expr,$typ:ty)=>{
        $x as $typ + $y as $typ
    }
}

fn main(){
    let i:u8 = 5;
    let j:i32 = 10;
    println!("{}",add_using!(i,j,i32));
}

Repeat / Dynamic Arguments

($($v:expr),*)  - Here the star (*) will repeat the patterns inside $()
And comma is the separator.
// Repeat / Dynamic number of arguments

macro_rules! hi {
    ($($name:expr),*) => {
        {
            //let mut n = Vec::new();
            $(
                println!("Hi {}!",$name);
            )*
        }
    };
}

fn main() {
    hi!("Rachel","Ross","Monica");
}

Remember vec! Macro?

Let's try to create our equivalent of it.

// Creating vec! equivalent 

macro_rules! my_vec {
    (
        $( $name:expr ), * ) => {
        {
            let mut n = Vec::new();
            $( n.push($name); )*
            
            n
        }
    };
}

fn main() {
    println!("{:?}",my_vec!("Rachel","Ross","Monica"));
}

Repeat - with Numeric arguments

macro_rules! add_all{
    ($($a:expr) , *) => 
    {
    // initial value of the expression
    0
    // block to be repeated
    $(+$a)*
    }
}

//The * in $(+$a)* is a repetition operator, meaning 
//"repeat +$a for each $a matched".

fn main(){
    println!("{}",add_all!(1,2,3,4));
    //println!("{}",add_all!());
}

Compile time Assertions

macro_rules! assert_equal_len {
    ($a:expr, $b:expr) => {
        assert!($a.len() == $b.len(), "Arrays must have the same length");
    };
}

fn main() {
    let a1 = [1, 2, 3];
    let a2 = [4, 5, 6];
    assert_equal_len!(a1, a2); // Compiles fine

    //let a3 = [7, 8];
    //assert_equal_len!(a1, a3); // This will fail to compile
}

Macro Overloading

Overload to accept different combinations of arguments

// `check!` will compare `$left` and `$right`
// in different ways depending on how you invoke it:

macro_rules! check {

    ($left:expr; and $right:expr) => {
        println!("{:?} and {:?} is {:?}",
                 stringify!($left), stringify!($right), $left && $right)
    };

    ($left:expr; or $right:expr) => {
        println!("{:?} or {:?} is {:?}",
                 stringify!($left), stringify!($right), $left || $right)
    };
}

fn main() {
    check!(1i32 + 1 == 2i32; and 2i32 * 2 == 4i32);
    check!(true; or false);
}

Generics

Monomorphization is a process where the compiler replaces generic placeholders with concrete datatypes.

Generics are a way to make a function or a type work for multiple types to avoid code duplication.

  • Doesn't affect Runtime Performance.
  • Generics are a zero-cost abstraction.
  • Make Programming easier without reducing runtime performance.

use std::fmt::Debug;
use std::fmt::Display;
use std::ops::*;

fn main() {
    //i32
    let n1: i32 = 10;
    let n2: i32 = 20;
    print_this(n1, n2);
    
    println!("Adding i32 ( {} + {} ) = {}", n1,n2, add_values(n1, n2));
    println!("Subtracting i32 ( {} from {} ) = {}", n1,n2,sub_values(n1, n2));
    println!("-----------------------------------");

    // F64
    let t1 = 23.45;
    let t2 = 34.56;
    print_this(t1, t2);

    println!("Adding f64 ( {} + {} ) = {}", t1,t2, add_values(t1, t2));
    println!("Subtracting f64 ( {} from {} ) = {}", t1,t2,sub_values(t1, t2));
    println!("-----------------------------------");

    // &str
    let r1 = "Rachel";
    let r2 = "Green";
    print_this(r1, r2);
    println!("----------------");

    // String Object
    let s1 = String::from("Rachel");
    let s2 = String::from("Green");
    print_this(s1, s2);
    println!("----------------");
    
    //printing diff datatypes
    
    print_another(t1,n1);

}


fn print_this<T: Debug + Display>(p1: T, p2: T) -> () {
    println!("Printing - {:?},{}", p1, p2)
}

fn add_values<T: Add<Output = T>>(p1: T, p2: T) -> T {
    // ://doc.rust-lang.org/std/ops/index.html
    p1 + p2
}

fn sub_values<T: Sub<Output = T>>(p1: T, p2: T) -> T {
    p1 - p2
}

fn print_another<T: Debug + Display, U:Debug + Display>(p1: T, p2: U) -> () {
    println!("Printing Diff Datatypes - {:?},{}", p1, p2)
}

Generics with Struct

// Generics with Struct

struct Sample<T, U, V> {
    a: T,
    b: U,
    c: V
}

fn main() {
    let var1 = Sample{ a: 43.10, b: 2, c:"Hello" };
    println!("A: {}", var1.a);
    println!("B: {}", var1.b);
    println!("C: {}", var1.c);

    println!("----------------");

    let var2 = Sample{ a: "Hello", b: "World", c:34.6 };
    println!("A: {}", var2.a);
    println!("B: {}", var2.b);
    println!("C: {}", var2.c);
}

Traits

A trait defines a particular type's functionality and can share with other types. We can use traits to define shared behavior abstractly.

Traits allow us to define interfaces or shared behaviors on types. To implement a trait for a type, we must implement methods of that trait.

When working with Structs, we have enabled Debug and Display traits.

Rust supports defining and using custom traits.

// Display Struct without Trait

#[derive(Debug)]

struct Show {
    name: String,
    total_seasons: u8 
}

#[derive(Debug)]
struct Season {
    season_name: String,
    season_id: u8,
    year: u16
}


fn main(){
	let friends = Show {
		name:String::from("Friends"),
		total_seasons:10
	};

	let currseason = Season {
		season_name:String::from("Last Season"),
		season_id:10,
		year:2004
	};
	
	println!("{:?}", friends);
	println!("{:?}", currseason);
}
// Implementing the same using Traits

struct Show {
    name: String,
    total_seasons: u8 
}

struct Season {
    season_name: String,
    season_id: u8,
    year: u16
}

//We will list the method signature for all the methods that a type 
// implementing the Info trait will need to have.

trait Info {
	// Any datatype that implements this "detail" trait will return a string.

	fn detail(&self) -> String;
}

impl Info for Show{
	fn detail(&self) -> String{
		format!("{} contains {} seasons",self.name,self.total_seasons)
	}
}

impl Info for Season{
	fn detail(&self) -> String{
		format!("you are watching {} ({}) telecasted in {}", self.season_name, self.season_id, self.year)
	}
}

fn main(){
	let friends = Show {
		name:String::from("Friends"),
		total_seasons:10
	};

	let currseason = Season {
		season_name:String::from("Last Season"),
		season_id:10,
		year:2004
	};
	
	
	println!("{}", friends.detail());
	println!("{}", currseason.detail());
}

Polymorphism & Generics


struct Show {
    name: String,
    total_seasons: u8 
}

struct Season {
    season_name: String,
    season_id: u8,
    year: u16
}

//We will list the method signature for all the methods that a type 
// implementing the Info trait will need to have.

trait Info {
	// Any datatype that implements this "detail" trait will return a string.

	fn detail(&self) -> String;
}

impl Info for Show{
	fn detail(&self) -> String{
		format!("{} contains {} seasons",self.name,self.total_seasons)
	}
}

impl Info for Season{
	fn detail(&self) -> String{
		format!("you are watching {} ({}) telecasted in {}", self.season_name, self.season_id, self.year)
	}
}

// A generic function that works with any type that implements the Describe trait
fn print_description<T: Info>(item: T) {
    println!("{}", item.detail());
}

fn main(){
	let friends = Show {
		name:String::from("Friends"),
		total_seasons:10
	};

	let currseason = Season {
		season_name:String::from("Last Season"),
		season_id:10,
		year:2004
	};
	
	print_description(friends);
	print_description(currseason);
}

Defining Shared Behavior: Traits allow you to define a set of methods that can be shared across multiple types. Any type that implements a trait must provide the specified behavior, ensuring consistency across types.

Abstraction: Traits enable abstraction by allowing you to write generic code that can operate on any type that implements a particular trait.

Polymorphism: Traits support polymorphism, which allows you to write code that can work with different types in a uniform manner.

Extensibility: Traits provide a way to extend existing types with new behavior without modifying their original implementation.

Default Implementation

In some cases, it's useful to have a default implementation for one or more of the methods in a trait.

Especially when you have a trait with many methods, you can implement only some of them for every datatype.

// Default Trait

struct Show {
    name: String,
    total_seasons: u8 
}

struct Season {
    season_name: String,
    season_id: u8,
    year: u16
}


trait Info {
	fn detail(&self) -> String;

	fn description(&self) -> String {
		String::from("No description available")
	}
}

impl Info for Show{
	fn detail(&self) -> String{
		format!("{} contains {} seasons",self.name,self.total_seasons)
	}

}

impl Info for Season{
	fn detail(&self) -> String{
		format!("you are watching {} ({}) telecasted in {}", self.season_name, self.season_id, self.year)
	}

	fn description(&self) -> String{
		format!("Its the series finale episode.")
	}
}



fn main(){
	let friends = Show {
		name:String::from("Friends"),
		total_seasons:10
	};

	let currseason = Season {
		season_name:String::from("Last Season"),
		season_id:10,
		year:2004
	};
	
	
	println!("Friends - Detail : {}", friends.detail());
	println!("Current Season - Detail {}", currseason.detail());
    	println!("---------------------");
	println!("Printing Default Description : {}", friends.description());
	println!("Printing specific Description : {}", currseason.description());

}

Derivable Traits

Provide default implementations for several common traits

The compiler will generate default code for the required methods when you derive traits.

If you need something specific, you'll need to implement the methods yourself.

List of commonly used derivable traits

  • Eq
  • PartialEq
  • Ord
  • PartialOrd
  • Clone
  • Copy
  • Hash
  • Default
  • Debug
// Comparison

#[derive(PartialEq,PartialOrd)]

struct Show {
    name: String,
    total_seasons: u8 
}

fn main(){
	let friends = Show {
		name:String::from("Friends"),
		total_seasons:10
	};

	let bbt = Show {
		name:String::from("BBT"),
		total_seasons:12
	};
		
	println!("{}", friends == bbt);
	println!("{}", friends > bbt);	

}

What if we need to have custom comparison on specific items

// Custom Comparison

#[allow(dead_code)]

struct Show {
    name: String,
    total_seasons: u8,
}

// self = self: Self and &self = self: &Self

trait Comparison {
    fn eq(&self, obj1:&Self) -> bool;
}

impl Comparison for Show {
    fn eq(&self, obj1: &Self) -> bool {
        if self.total_seasons == obj1.total_seasons {
            true
        } else {
            false
        }
    }
}

fn main() {
    let friends = Show {
        name: String::from("Friends"),
        total_seasons: 10,
    };

    let bbt = Show {
        name: String::from("BBT"),
        total_seasons: 12,
    };

    println!("Custom Comparison {}", friends.eq(&bbt));
}

Another Example with Numerical values

//declare a structure

struct Circle {
    radius: f32,
}

struct Rectangle {
    width: f32,
    height: f32,
}

struct Square {
    width: f32,
}

//declare a trait

trait Area {
    fn shape_area(&self) -> f32;
}

//implement the trait

impl Area for Square {
    fn shape_area(&self) -> f32 {
        self.width * self.width
    }
}

impl Area for Circle {
    fn shape_area(&self) -> f32 {
        3.14 * self.radius * self.radius
    }
}

impl Area for Rectangle {
    fn shape_area(&self) -> f32 {
        self.width * self.height
    }
}

fn main() {
    //create an instance of the structure
    let c = Circle { radius: 2.0 };

    let r = Rectangle {
        width: 2.0,
        height: 2.0,
    };

    let s = Square { width: 5.0 };

    println!("Area of Circle: {}", c.shape_area());
    println!("Area of Rectangle:{}", r.shape_area());
    println!("Area of Rectangle:{}", s.shape_area());
}

Database

Why use System Programming language for Database Development?

Rust is known for its safety and performance, which makes it great for database applications.

  1. Drivers and ORMs: Rust has solid drivers for major databases like PostgreSQL, MySQL, and SQLite. You've got ORMs (Object-Relational Mappers) like Diesel which makes database interactions more Rust-like and safe.
  2. Async/Await: Rust's async programming model works well with databases, especially for web apps. Libraries like tokio-postgres offer async database access.
  3. Safety and Concurrency: Rust's focus on safety and its powerful concurrency model can help prevent common database-related bugs, like race conditions.
  4. Custom Database Systems: Some folks even build custom database systems in Rust, thanks to its low-level control and efficiency.
  5. Web Frameworks Integration: With Web frameworks like Actix and Rocket, integrating databases into web applications is straightforward in Rust.

Free Cloud-based Databases (MySQL, PostgreSQL, CouchDB, RabbitMQ)

https://www.alwaysdata.com/en/register/?d

------

Read Environment Variables

// SET Environment Variables

// Linux or MAC

// export MY_POSTGRESQL_USERID=value

// Windows

// Goto Environment Variables and Add

fn main() {
    println!("{}",std::env::var("MY_POSTGRESQL_USERID").unwrap());
}

cargo.toml

postgres="0.19.7"

Create Table

use postgres::{Client, Error, NoTls};

fn main() -> Result<(), Error> {

    // clearscreen::clear().expect("failed to clear screen");

    let postgresql_userid = std::env::var("MY_POSTGRESQL_USERID").unwrap();
    let postgresql_pwd = std::env::var("MY_POSTGRESQL_PWD").unwrap();

    let conn_str = format!("postgresql://{postgresql_userid}:{postgresql_pwd}@postgresql-dbworldgc.alwaysdata.net:5432/dbworldgc_pg");

    let mut client = Client::connect(&conn_str, NoTls)?;

    client.batch_execute(
        "
    CREATE TABLE if not exists sitcoms (
        id      SERIAL PRIMARY KEY,
        name    TEXT NOT NULL,
        genre    TEXT NULL
    )
",
    )?;

    Ok(())
}

NoTls is a type provided by the postgres crate in Rust, which specifies that the connection to the PostgreSQL database should not use TLS (Transport Layer Security) encryption. Essentially, it indicates that the connection should be made without any encryption.

Unencrypted Connection: Non prod or Used in Trusted environment. Performance: Faster due to absence of encryption overhead.

use postgres::{Client, Error, NoTls};

fn main() -> Result<(), Error> {

    clearscreen::clear().expect("failed to clear screen");

    let postgresql_userid = std::env::var("MY_POSTGRESQL_USERID").unwrap();
    let postgresql_pwd = std::env::var("MY_POSTGRESQL_PWD").unwrap();

    let conn_str = format!("postgresql://{postgresql_userid}:{postgresql_pwd}@postgresql-dbworldgc.alwaysdata.net:5432/dbworldgc_pg");

    let mut client = Client::connect(&conn_str, NoTls)?;
        
    //INSERT

    let name = "Friends";
    let genre = "RomCom";

    client.execute(
        "INSERT INTO sitcoms (name, genre) VALUES ($1, $2)", &[&name, &genre],
    )?;

    //UPDATE

    let genre = "Comedy";
    let id = 2;

    client.execute(
         "UPDATE sitcoms SET genre = $2 WHERE id = $1", &[&id, &genre],
    )?;

    //DELETE

    let id = 1;
    client.execute(
        "DELETE FROM sitcoms WHERE id = $1", &[&id],
    )?;


    // Multiple Rows Insert

    let tup_arr = [("Seinfeld","Standup"),("Charmed","Drama")];

    for row in tup_arr{
        client.execute(
            "INSERT INTO sitcoms (name, genre) VALUES ($1, $2)", &[&row.0, &row.1],
        )?;
        println!("Inserting --- {},{}",row.0,row.1);
    }

    // Read the Value

    for row in client.query("SELECT id, name, genre FROM sitcoms", &[])? {
        let id: i32 = row.get(0);
        let name: &str = row.get(1);
        let genre: &str = row.get(2);
        println!("---------------------------------");
        println!("{} | {} | {:?}", id, name, genre);
    }


    #[derive(Debug)]
    struct Sitcom {
        id: i32,
        name: String,
        genre: Option<String>, // Use Option for nullable fields
    }


    // Read the Value and store in a vector
    let mut sitcoms: Vec<Sitcom> = vec![];
    
    for row in client.query("SELECT id, name, genre FROM sitcoms", &[])? {
         let sitcom = Sitcom {
             id: row.get(0),
             name: row.get(1),
             genre: row.get(2),
         };
         sitcoms.push(sitcom);
     }

    // // Print the sitcoms vector
     for sitcom in sitcoms {
         println!("{:?}", sitcom);
     }

    Ok(())
}

Transactions

use postgres::{Client, Error, NoTls, Transaction};

fn main() -> Result<(), Error> {
    let postgresql_userid = std::env::var("MY_POSTGRESQL_USERID").unwrap();
    let postgresql_pwd = std::env::var("MY_POSTGRESQL_PWD").unwrap();
    let conn_str = format!("postgresql://{postgresql_userid}:{postgresql_pwd}@postgresql-dbworldgc.alwaysdata.net:5432/dbworldgc_pg");

    let mut client = Client::connect(&conn_str, NoTls)?;

    // Create table
    client.batch_execute(
        "
        CREATE TABLE IF NOT EXISTS public.sitcoms_tran (
            id      SERIAL PRIMARY KEY,
            name    TEXT UNIQUE NOT NULL,
            genre   TEXT NULL
        )
        ",
    )?;

    // Start a transaction
    let mut transaction = client.transaction()?;

    // Insert rows within the transaction
    match insert_sitcoms(&mut transaction) {
        Ok(_) => {
            // If no error, commit the transaction
            transaction.commit()?;
            println!("Transaction committed.");
        },
        Err(e) => {
            // If there is an error, rollback the transaction
            transaction.rollback()?;
            eprintln!("Transaction rolled back due to error: {}", e);
        }
    }

    Ok(())
}

fn insert_sitcoms(transaction: &mut Transaction) -> Result<(), Error> {
    transaction.execute("INSERT INTO sitcoms_tran (name, genre) VALUES ($1, $2)", &[&"Seinfeld", &"RomCom"])?;

    Ok(())
}

Error Handling

use postgres::{Client, Error, NoTls};

fn main() -> Result<(), Error> {
    let postgresql_userid = std::env::var("MY_POSTGRESQL_USERID").unwrap();
    let postgresql_pwd = std::env::var("MY_POSTGRESQL_PWD").unwrap();
    let conn_str = format!("postgresql://{postgresql_userid}:{postgresql_pwd}@postgresql-dbworldgc.alwaysdata.net:5432/dbworldgc_pg");

    let mut client = Client::connect(&conn_str, NoTls)?;

    client.batch_execute(
        "
        CREATE TABLE IF NOT EXISTS public.sitcoms (
            id      SERIAL PRIMARY KEY,
            name    TEXT NOT NULL,
            genre   TEXT NULL
        )
        ",
    )?;

    let result = client.execute("INSERT INTO sitcoms (name, genre) VALUES ($1, $2)", &[&"Friends", &"RomCom"]);

    match result {
        Ok(rows) => println!("Inserted {} row(s)", rows),
        Err(err) => eprintln!("Error inserting row: {}", err),
    }

    Ok(())
}

Smart Pointers

Box

A good use case for the Box type in Rust programming is when you have a large data structure that you want to store on the heap rather than on the stack.

The Box type allows you to store the data on the heap in a safe and efficient way.

One reason why using a Box might be beneficial is that it can help you avoid stack overflows. The stack is a fixed-size data structure, so if you try to store a large data structure on the stack, it can overflow and cause your program to crash.

Use Cases

  • Dynamic Sized DataTypes
  • Recursive Data Structures
  • Reducing Stack Usage
  • Enforcing Single Ownership. (instead of splitting with Stack and Heap)

Toy Box

What do you think about using a Toy Box?

Rust has a Smart Pointer called Box<T>. It allows developers to store data on the heap rather than the stack. What remains on the stack is the pointer to the heap data.

Boxes don't have performance overhead other than storing their data on the heap instead of on the stack.


fn main() {
    let speed = 88;
    let box_speed: Box<i32> = Box::new(speed);
    println!("speed {}, Stack location of speed {:p}", speed, &speed);
    println!("Stack location of box_speed {:p}", &box_speed);
    println!(
        "Heap location of value stored inside box_speed {:p}",
        Box::into_raw(box_speed)
    );
}

The as_ptr() method works on arrays and slices

fn main() {

    let large_array: [i32; 5] = [1,2,3,4,5];
    let large_array1: Box<[i32; 5]> = Box::new([1, 2, 3, 4, 5]);

    println!("Stack Memory Location of variable large_array : {:p} {:p} {:?}", &large_array, large_array.as_ptr(), large_array);
    
    println!("Heap Memory Location of variable large_array1 : {:p} {:p} {:?}", &large_array1,  large_array1.as_ptr(), large_array1);

}
// Better use case for using Box

use std::mem;

#[allow(dead_code)]
#[derive(Debug)]

struct Class {
    id: i32,
    fname: String,
    lname: String,
}

fn main() {
    let c = Class {
        id: 34,
        fname: String::from("Rachel"),
        lname: String::from("Green"),
    };

    println! {"{:?}", c};

    // Find the Size of the Variable in the Stack
    println!("Class size on stack: {:p} {} bytes",&c,mem::size_of_val(&c));

    let boxed_class: Box<Class> = Box::new(c);
    // Size of the Boxed Variable in Stack pointing to Heap.
    println!("boxed_class size on stack: {:p} {} bytes",&boxed_class,mem::size_of_val(&boxed_class));

    // Size of the Boxed Variable in Heap
    println!("boxed_class size on heap: {} bytes", mem::size_of_val(&*boxed_class) );

    let unbox_class: Class = *boxed_class;
    println!("unbox class size on stack: {} bytes", mem::size_of_val(&unbox_class));
}
// Stack to Heap

#[allow(dead_code)]
#[derive(Debug)]
struct Class {
    id: i32,
    fname: String,
    lname: String,
}

fn main() {
    let c = Class {
        id: 34,
        fname: String::from("Rachel"),
        lname: String::from("Green"),
    };
    
    let a = 10;
    let s = String::from("Hello");
    
    // Stack
    println!("Sample Stack location {:p}", &a);
    // Heap
    println!("Sample Heap location {:p}", s.as_ptr());

    println!("\nBefore Boxing the Struct \n");
    println!("Stack Memory Location of Struct variable c : {:p}\nStruct Value : {:?}", &c, c);
    println!("Stack Location of c.id : {:p}", &c.id);
    println!("Stack c.fname pointing to : {:p} \nHeap c.fname {:p}", &c.fname,c.fname.as_ptr());
    println!("Stack c.lname pointing to : {:p} \nHeap c.lname {:p}", &c.lname,c.lname.as_ptr());
    
    println!("\nAfter Boxing the Struct \n");
    let b = Box::new(Class {
        id: 34,
        fname: String::from("Rachel"),
        lname: String::from("Green"),
    });
    println!("Heap Memory Location  {:p}\nStruct Value {:?}", b, b);
    println!("\n");
    println!("Heap Location of b.id : {:p}", &b.id);
    println!("Heap b.fname pointing to : {:p} \nHeap Location b.fname {:p}",&b.fname,b.fname.as_ptr());
    println!("Heap b.lname pointing to : {:p} \nHeap Location b.lname {:p}",&b.lname,b.lname.as_ptr());
}

Linked List Example

#![allow(unused)]
fn main() {
// Linked List

Here is an example of a linked list that could be stored on the stack in Rust:

struct Node {
    value: i32,
    next: Option<Box<Node>>,
}

struct LinkedList {
    head: Option<Box<Node>>,
}
}

In this example, the Node struct represents a single node in the linked list, and the LinkedList struct represents the entire linked list. The head field of the LinkedList struct contains a reference to the first node in the list, and each Node struct has a next field that contains a reference to the next node in the list.

Because this data structure is relatively small, it can be stored on the stack without problems. However, if the linked list became very large, it could cause a stack overflow, in which case it would be better to store it on the heap using a Box type.

Log4J

Log4j is a popular logging library for Java and is widely used in Java-based applications. It is famous for several reasons, including its ability to output logs in various formats, its support for customizable log levels, and its ability to integrate with other logging frameworks.

It started as a logging tool for Java, which eventually became a de facto standard in the industry.

Rust developers wrote a wrapper around those .jar files and made it available for Rust also.

https://github.com/Better-Player/log4j-rs

Several tools can be used to view the Logs generated by log4j.

LogViewPlus https://www.logviewplus.com/log-viewer.html

Another one https://www.xplg.com/log4j-viewer-analyzer/

Apart from this, there are a lot of Big Data Real-time log viewers, such as

Splunk

Logstash

-------

Appenders route log messages to different destinations, such as a log file, the console, or a network socket.

// cargo.toml

[dependencies]
log="0.4.17"
log4rs = "1.2.0"
// main.rs

use log::{error,info,warn,trace,debug, LevelFilter, SetLoggerError};
use log4rs::{
	append::{
		console::{ConsoleAppender},
		file::FileAppender,		
	},
	config::{Appender, Root,Config,Logger},
	encode::pattern::PatternEncoder,
    encode::json::JsonEncoder,
};

fn main() -> Result<(), SetLoggerError>  {

    // https://docs.rs/log4rs/latest/log4rs/encode/pattern/
    let stdout = ConsoleAppender::builder()
                    //.encoder(Box::new(JsonEncoder::new()))
                    .encoder(Box::new(PatternEncoder::new("{d} {h({l})} - {f} Line:{L} - {t} - {m}{n}")))
                    .build();

    let tofile = FileAppender::builder()
                    .encoder(Box::new(PatternEncoder::new("{d} {h({l})} - {f} Line:{L} - {t} - {m}{n}")))
                    .encoder(Box::new(JsonEncoder::new()))
                    .build("log/requests.log")
                    .unwrap();

    let config = Config::builder()
                    .appender(Appender::builder().build("stdout", Box::new(stdout)))
                    .appender(Appender::builder().build("save_to_file", Box::new(tofile)))
                    
                    .logger(Logger::builder()
                        .appender("save_to_file")
                        .additive(false)
                        .build("requests", LevelFilter::Warn))

                    .logger(Logger::builder()
                        .appender("stdout")
                        .additive(false)
                        .build("stdout", LevelFilter::Warn))

                    .build(
                        Root::builder()
                            .appender("stdout")
                            .appender("save_to_file")
                            .build(LevelFilter::Trace)
                        )
                    .unwrap();

    let _handle = log4rs::init_config(config).unwrap();

    //

    info!("Just FYI");
    error!("Used when reporting errors / panic situations.");
    warn!("Non Critical messages");
    debug!("Use this when debugging scripts");
    trace!("This is also used when debugging scripts. The difference is the granularity trace offers.");

    Ok(())
}

Closures

Rust closures are anonymous functions without any name that you can save in a variable or pass as arguments to other functions.

Also called headless functions.

| | represents its a Closure.

// Simple Closure Example

fn main() {
    let example = |num| -> i32 { num + 1 };
    println!("{}", example(5));
}

Function vs. Closure

// Function vs Closure

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

fn main() {
    //let y = 6;
    let add_one_cl = |x: i32| -> i32 { 1 + x};
    println!("Closure : {}", add_one_cl(3));
    println!("Function : {}", add_one_fn(3));
}

Unlike functions, closures can capture values from the scope in which they're called.

One more example

// Closure returns a value

fn main() {
    let s = String::from("Hello");

    let closure = |name: String| -> String {
        format!("{}, {}!", s, name)
    };

    let result = closure(String::from("Rachel"));
    println!("The result is: {}", result);
}
// Mutable Values inside Closure

fn main() {
    let plus_two = |x| {
        let mut result: i32 = x;

        result += 1;
        result += 1;

        result
    };

    println!("{}", plus_two(2));
}

Use Cases

  • Creating functions that can be passed around and used in different parts of your code without explicitly defining the function in every place you want to use it.

  • This can make your code more modular and easier to understand.

  • Capturing variables from the environment and using them in the closure's code.

  • This allows you to create functions that can access and use values from the scope in which they are defined, even after the code that defined the closure has finished executing.

  • Implementing complex behavior for a type without creating a new struct or type. For example, you can use a closure to define the behavior of a trait object, which allows you to define complex behavior without creating a new type to represent that behavior.

  • Creating a higher-order function takes other functions as arguments and return other functions as results.

Overall, closures are a very useful tool in Rust, and they can be used to solve a wide range of problems in your code.

Map and Collect

Map: The map function is used to transform elements of an iterator. It takes a closure and applies this closure to each element of the iterator, producing a new iterator with the transformed elements.

Collect: The collect function is used to transform an iterator into a collection, such as a Vec, HashMap, or HashSet. It consumes the iterator and returns the new collection.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    println!("Mapping: {:?}",numbers.iter().map(|x| x * 2));
    
    
    let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
    println!("Map and Collect {:?}", doubled);
}
  • numbers.iter() creates an iterator over the elements of the numbers vector.
  • .map(|x| x * 2) transforms each element by multiplying it by 2.
  • .collect() collects the transformed elements into a new Vec.

Nice to Know

Important Concepts

Date format

Epoch Time: Unix Time is the number of seconds that have elapsed since Jan 1 1970 UTC.

https://www.epochconverter.com/

It uses 32 Bit Integer, so by Jan 19, Y2038 we will run out of storage and companies gradually upgrading it as they upgrade their systems.

  • Binary : 0 - 1

  • Octal : 0 - 7

  • Hex : 0 - 9, A - F

  • Base 10 a.k.a Decimal Numbers : 0 - 9

  • Base 36 : 0 - 9 + A - Z

Base 36 is the most compact case-insensitive alphanumeric numbering system.

Popular Use Cases:

  • Base 36 is used for Dell Express Service Codes, website url shorteners and many other applications which have a need to minimize human error.
  • Reduce cloud storage cost.

https://www.rapidtables.com/convert/number/base-converter.html

Example: Processing 1 billion rows each hour for a day

Billion rows x 14 = 14 billion bytes = 14 GB x 24 hrs = 336 GB

Billion rows x 8 = 8 billion bytes = 8 GB x 24 hrs = 192 GB

Base 64:

Base64 encoding schemes are commonly used when there is a need to encode binary data that needs be stored and transferred over media that are designed to deal with textual data. This is to ensure that the data remains intact without modification during transport.

Base64 is a way to encode binary data into an ASCII character set known to pretty much every computer system, in order to transmit the data without loss or modification of the contents itself.

2 power 6 = 64

So Base64 Binary values are six bits not 8 bits.

Img Source

Base64 encoding converts every three bytes of data (three bytes is 3*8=24 bits) into four base64 characters.

Example:

Convert Hi! to Base64

Character - Ascii - Binary

H= 72 = 01001000

i = 105 = 01101001

! = 33 = 00100001

Hi! = 01001000 01101001 00100001

Split into 4 parts

010010 000110 100100 100001

S G k h

https://www.base64encode.org/

How about converting Hi to Base64

010010 000110 1001

Add zeros in the end so its 6 characters long

010010 000110 100100

Base 64 is SGk=

= is the padding character so the result is always multiple of 4.

Another Example

convert f to Base64

102 = 01100110

011001 100000

Zg==

Think about sending Image (binary) as JSON, binary wont work. But sending as Base64 works the best.

https://elmah.io/tools/base64-image-encoder/

Demo Img HTML

base64 = "0.22.1"
// Convert to Base64

use base64::encode;
fn main() {
    let string = "Hello world!";
    let encoded = encode(string);
    println!("Base64: {}", encoded);
    
    let bin_string = b"Hello world!";
    let encoded1 = encode(bin_string);
    println!("Base64: {}", encoded1);
       
}

Convert Base64 to String

// Convert to String

use base64::{encode,decode};
use std::str;

fn main() {
    let string = "Hello world!";
    let encoded = encode(string);
    println!("Base64: {}", encoded);
    
    let decoded = decode("SGVsbG8gd29ybGQh").unwrap();
    println!("{:?}",decoded);
    let converted_string = str::from_utf8(&decoded).unwrap();
    println!("{:?}",converted_string);

}

Image to Base64

cargo add base64

Store a image named sample.jpg at the same level as src (not inside src)

use std::fs::File;
use std::io::{self, Read, Write};
use base64::{engine::general_purpose, Engine as _};

fn main() -> io::Result<()> {
    // Read the image file
    let mut image_file = File::open("sample.jpg")?;
    let mut image_data = Vec::new();
    image_file.read_to_end(&mut image_data)?;

    // Encode the image data to Base64
    let base64_encoded = general_purpose::STANDARD.encode(&image_data);

    // Write the Base64 encoded string to a new file
    let mut output_file = File::create("output_base641.txt")?;
    output_file.write_all(base64_encoded.as_bytes())?;

    println!("Base64 encoded image data has been written to output_base64.txt");

    Ok(())
}

Read from Base64 to Image

use std::fs::File;
use std::io::{self, Read, Write};
use base64::{engine::general_purpose, Engine as _};

fn main() -> io::Result<()> {
    // Read the Base64 encoded text file
    let mut base64_file = File::open("output_base64.txt")?;
    let mut base64_string = String::new();
    base64_file.read_to_string(&mut base64_string)?;

    // Decode the Base64 string to binary image data
    let image_data = general_purpose::STANDARD.decode(&base64_string).expect("Failed to decode Base64 string");

    // Write the binary image data to a new image file
    let mut output_image_file = File::create("decoded_image.jpg")?;
    output_image_file.write_all(&image_data)?;

    println!("Image has been decoded and written to decoded_image.jpg");

    Ok(())
}

Big O Notation

Algorithmic Complexity

When analyzing an algorithm,

Time Complexity: The time it takes to execute the code. Space Complexity: The space taken in the memory to execute the code.

Following Notations are used to represent Algorithmic Complexity. Big O is what everybody is interested in.

Big - Omega = Best Case Big - Theta = Average Case BIG O = Worst Case

Will try to use general algorithms not any specific programming syntaxes.

Constant or Static Complexity - O(1)

// Defining a constant
const FAHRENHEIT_CONSTANT: f64 = 32.0;

// Defining a static variable
static MULTIPLIER: f64 = 1.8;

fn main() {
    println!("Enter Name:");

    // Example temperature conversion calculations
    let fahrenheit: f64 = 100.0; // Example input
    let celsius = fahrenheit_to_celsius(fahrenheit);
    let fahrenheit_converted_back = celsius_to_fahrenheit(celsius);

    println!("Celsius: {:.2}, Fahrenheit: {:.2}", celsius, fahrenheit_converted_back);
}

fn fahrenheit_to_celsius(f: f64) -> f64 {
    (f - FAHRENHEIT_CONSTANT) / MULTIPLIER
}

fn celsius_to_fahrenheit(c: f64) -> f64 {
    (c * MULTIPLIER) + FAHRENHEIT_CONSTANT
}

Each line is of complexity O(1). Because its handling only one item.

n * O(1);

n is the number of lines.

While finding the pattern we ignore the constant values.

So we remove n and the complexity is O(1)

Linear Complexity O(N)

In this case the time and size changes based on number of input values.

For example

// Linear Complexity

for i = 1 to N
print (i)

if N = 10 it will be print faster, if N = 1Million the time taken will be linear.

These kinds of Linear changes is called O(N)

fn main() {
    // Create an array of integers
    let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Call the function to print the elements
    print_elements(&numbers);
}

fn print_elements(numbers: &[i32]) {
    // Iterate over the elements of the array
    for number in numbers.iter() {
        println!("{}", number);
    }
}

Quadratic Complexity

// Quadratic Complexity

for i = 1 to N
    for j = 1 to M
        print (i,j)   
fn main() {
    let n = 5; // Example value for N
    let m = 5; // Example value for M

    print_pairs(n, m);
}

fn print_pairs(n: usize, m: usize) {
    for i in 1..=n {
        for j in 1..=m {
            println!("({}, {})", i, j);
        }
    }
}

For every i, there is another loop called j

N * N = O(N Square)

If N = 2 then the process will iterate 4 times.

What is the Complexity of these ?

for i = 1 to n
print (i)

for j = 1 to n
print (j)
 
for (i = 0; i < N; i++) {
    for (j = 0; j < N; j++) {
        sequence of statements
    }
}
for (k = 0; k < N; k++) {
    sequence of statements
}
for i = 1 to N
    for j = 1 to M
        for k = 1 to 1000
            print (i,j,k)
        

Exponential Complexity

O(2 power N)

With the increase in input there is an exponential growth in Time and Space.

Fibonacci Series

0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610

Algorithm

function fibonacci(n){
    if n = 0 
        return 0
    if n = 1 
        return 1
    else
        return fibonacci(n - 2) + fibonacci(n - 1)
fn fibonacci(n: u32) -> u32 {
    if n == 0 {
        return 0;
    }
    if n == 1 {
        return 1;
    }
    return fibonacci(n - 2) + fibonacci(n - 1);
}

fn main() {
    let n = 6; 
    println!("Fibonacci series up to {}:", n);
    for i in 0..=n {
        println!("Fibonacci({}) = {}", i, fibonacci(i));
    }
}

For example

(2 power n-1)

For input = 3, number of iterations is 4

graph TD
    A[Fibonacci 3] --> B[Fibonacci 1]
    A --> C[Fibonacci 2]
    C --> D[Fibonacci 0]
    C --> E[Fibonacci 1]

For input = 4, number of iterations is 8;

graph TD
    A[Fibonacci 4] --> B[Finonacci 3]
    A --> C[Fibonacci 2]
    B --> D[Fibonacci 2]
    B --> E[Fibonacci 1]
    D --> F[Fibonacci 1]
    D --> G[Fibonacci 0]
    C --> H[Fibonacci 1]
    C --> I[Fibonacci 0]

Logarithmic Complexity O(Log N)

Increase in number of input is exponential but time and space growth is Linear.

for (i= 1; i< n; i = i **2)
    print(i)

or Binary Search

1 23 45 56 89 90 110 130

Pick mid point, search either left or right.

fn binary_search(arr: &[i32], target: i32) -> Option<usize> {
    let mut low = 0;
    let mut high = arr.len() - 1;

    while low <= high {
        let mid = low + (high - low) / 2;

        if arr[mid] == target {
            return Some(mid);
        } else if arr[mid] < target {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }

    None
}

fn main() {
    let arr = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 20, 21, 24, 25, 30, 44,55,56];
    let target = 17;

    match binary_search(&arr, target) {
        Some(index) => println!("Target {} found at index: {}", target, index),
        None => println!("Target {} not found in the array", target),
    }
}

bigocheatsheet.com

Fore more visit

https://bigocheatsheet.com

Answers

  • O(1)
  • O(N sq)
  • O(N x M)

Cargo Publish

Note: Once you publish a crate, there is no removal.

Visit https://crates.io/

  • Login with GitHub
  • Verify the email address
  • Goto Account > API Settings and Generate a new token.

Epoch to human

Sample Libraries

Epoch to Human

Hello Lib

// Cargo Commands

cargo login <token>
cargo publish --dry-run

// git commands

git add Cargo.toml
git add README.md
git add src/lib.rs
git add .gitignore
git commit -m "Demo Crate"

// Publish

cargo publish

Cargo Profiles

Profiles provide a way to alter the compiler settings, influencing things like optimizations and debugging symbols.

Cargo has 4 built-in profiles

  • dev
  • release
  • test
  • bench

https://doc.rust-lang.org/cargo/reference/profiles.html

[profile.dev]
opt-level = 0
debug = true
debug-assertions = true
overflow-checks = true
incremental = true


[profile.release]
opt-level = 3
debug = false
debug-assertions = false
overflow-checks = false
incremental = false

Custom Profiles

// 

[profile.release-panic]
inherits = 'release'
panic = 'abort'

The specifications are selected based on the type of build.

// dev profile is chosen
cargo build  

//release profile is chosen
cargo build --release

//Custom Profile
cargo build --profile release-panic

Cargo Watch

This enables dynamic compilation as soon as the project is saved. This helps to save compilation time.

Goto command prompt or terminal and

// How to install Cargo Watch

cargo install cargo-watch

Goto respective project

Dynamic Build

cargo watch -x check

Dynamic Build and Run

cargo watch -x check -x run

Dynamic Build, Run and Test

cargo watch -x check -x run -x test

Cargo Audit

Goto command prompt or terminal and

// How to install Cargo audit

cargo install cargo-audit

Go to the project you want to audit

cargo audit

Check the Vulnerable crates and fix as suggested.

Example:

Goto osinfo project

Change the dependency to 0.1.0

Run the cargo audit

Check the error messages.

HTTP & REST API

HTTP Basics

HTTP (HyperText Transfer Protocol) is the foundation of data communication on the web, used to transfer data (such as HTML files and images).

GET - Navigate to a URL or click a link in real life.

POST - Submit a form on a website, like a username and password.

Popular HTTP Status Codes

  • 200 Series (Success): 200 OK, 201 Created.

  • 300 Series (Redirection): 301 Moved Permanently, 302 Found.

  • 400 Series (Client Error): 400 Bad Request, 401 Unauthorized, 404 Not Found.

  • 500 Series (Server Error): 500 Internal Server Error, 503 Service Unavailable.

Statefulness

Statefulness & Stickiness

The server stores information about the client's current session in a stateful system. This is common in traditional web applications. Here's what characterizes a stateful system:

Session Memory: The server remembers past interactions and may store session data like user authentication, preferences, and other activities.

Server Dependency: Since the server holds session data, the same server usually handles subsequent requests from the same client. This is important for consistency.

Resource Intensive: Maintaining state can be resource-intensive, as the server needs to manage and store session data for each client.

Example: A web application where a user logs in, and the server keeps track of their authentication status and interactions until they log out.

In this diagram:

Initial Request: The client sends the initial request to the load balancer.

Load Balancer to Server 1: The load balancer forwards the request to Server 1.

Response with Session ID: Server 1 responds to the client with a session ID, establishing a sticky session.

Subsequent Requests: The client sends subsequent requests with the session ID.

Load Balancer Routes to Server 1: The load balancer forwards these requests to Server 1 based on the session ID, maintaining the sticky session.

Server 1 Processes Requests: Server 1 continues to handle requests from this client.

Server 2 Unused: Server 2 remains unused for this particular client due to the stickiness of the session with Server 1.

Stickiness (Sticky Sessions)

Stickiness or sticky sessions are used in stateful systems, particularly in load-balanced environments. It ensures that requests from a particular client are directed to the same server instance. This is important when:

Session Data: The server needs to maintain session data (like login status), and it's stored locally on a specific server instance.

Load Balancers: In a load-balanced environment, without stickiness, a client's requests could be routed to different servers, which might not have the client's session data.

Trade-off: While it helps maintain session continuity, it can reduce the load balancing efficiency and might lead to uneven server load.

Methods of Implementing Stickiness

Cookie-Based Stickiness: The most common method, where the load balancer uses a special cookie to track the server assigned to a client.

IP-Based Stickiness: The load balancer routes requests based on the client’s IP address, sending requests from the same IP to the same server.

Custom Header or Parameter: Some load balancers can use custom headers or URL parameters to track and maintain session stickiness.

Statelessness

In a stateless system, each request from the client must contain all the information the server needs to fulfill that request. The server does not store any state of the client's session. This is a crucial principle of RESTful APIs. Characteristics include:

No Session Memory: The server remembers nothing about the user once the transaction ends. Each request is independent.

Scalability: Stateless systems are generally more scalable because the server doesn't need to maintain session information. Any server can handle any request.

Simplicity and Reliability: The stateless nature makes the system simpler and more reliable, as there's less information to manage and synchronize across systems.

Example: An API where each request contains an authentication token and all necessary data, allowing any server instance to handle any request.

In this diagram:

Request 1: The client sends a request to the load balancer.

Load Balancer to Server 1: The load balancer forwards Request 1 to Server 1.

Response from Server 1: Server 1 processes the request and sends a response back to the client.

Request 2: The client sends another request to the load balancer.

Load Balancer to Server 2: This time, the load balancer forwards Request 2 to Server 2.

Response from Server 2: Server 2 processes the request and responds to the client.

Statelessness: Each request is independent and does not rely on previous interactions. Different servers can handle other requests without needing a shared session state.

Monolithic Architecture

Definition: A monolithic architecture is a software design pattern in which an application is built as a unified unit. All application components (user interface, business logic, and data access layers) are tightly coupled and run as a single service.

Characteristics: This architecture is simple to develop, test, deploy, and scale vertically. However, it can become complex and unwieldy as the application grows.

Examples

  • Older/Traditional Banking Systems.

  • Enterprise Resource Planning (SAP ERP) Systems.

  • Content Management Systems like WordPress.

  • Legacy Government Systems. (Tax filing, public records management, etc.)

Advantages and Disadvantages

  • Advantages: Simplicity in development and deployment, straightforward horizontal scaling, and often more accessible debugging since all components are in one place.

  • Disadvantages: Scaling challenges, difficulty implementing changes or updates (especially in large systems), and potential for more extended downtime during maintenance.

Microservices

This approach structures an application as a collection of loosely coupled services. Microservices often favor stateless architectures for scalability and resilience.

Microservices architecture is a method of developing software applications as a suite of small, independently deployable services. Each service in a microservices architecture is focused on a specific business capability, runs in its process, and communicates with other services through well-defined APIs. This approach stands in contrast to the traditional monolithic architecture, where all components of an application are tightly coupled and run as a single service.

Critical Characteristics of Microservices:

Modularity: The application is divided into smaller, manageable pieces (services), each responsible for a specific function or business capability.

Independence: Each microservice is independently deployable, scalable, and updatable. This allows for faster development cycles and easier maintenance.

Decentralized Control: Microservices promote decentralized data management and governance. Each service manages its data and logic.

Technology Diversity: Teams can choose the best technology stack for their microservice, leading to a heterogeneous technology environment.

Resilience: Failure in one microservice doesn't necessarily bring down the entire application, enhancing the system's overall resilience.

Scalability: Microservices can be scaled independently, allowing for more efficient resource utilization based on demand for specific application functions.

Advantages:

Agility and Speed: Smaller codebases and independent deployment cycles lead to quicker development and faster time-to-market.

Scalability: It is easier to scale specific application parts that require more resources.

Resilience: Isolated services reduce the risk of system-wide failures.

Flexibility in Technology Choices: Microservices can use different programming languages, databases, and software environments.

Disadvantages:

Complexity: Managing a system of many different services can be complex, especially regarding network communication, data consistency, and service discovery.

Overhead: Each microservice might need its own database and transaction management, leading to duplication and increased resource usage.

Testing Challenges: Testing inter-service interactions can be more complex compared to a monolithic architecture.

Deployment Challenges: Requires robust DevOps practices, including continuous integration and continuous deployment (CI/CD) pipelines.

Idempotency

This is a concept where an operation can be applied multiple times without changing the result beyond the initial application. It's an essential concept in stateless architectures, especially for APIs.

REST API

REpresentational State Transfer is a software architectural style developers apply to web APIs.

REST APIs provide simple, uniform interfaces because they can be used to make data, content, algorithms, media, and other digital resources available through web URLs. Essentially, REST APIs are the most common APIs used across the web today.

Use of a uniform interface (UI)

HTTP Methods

GET: This method allows the server to find the data you requested and send it back to you.

POST: This method permits the server to create a new entry in the database.

PUT: If you perform the ‘PUT’ request, the server will update an entry in the database.

DELETE: This method allows the server to delete an entry in the database.

Sample REST API

https://api.zippopotam.us/us/08028

http://api.tvmaze.com/search/shows?q=friends


https://jsonplaceholder.typicode.com/posts

https://jsonplaceholder.typicode.com/posts/1

https://jsonplaceholder.typicode.com/posts/1/comments

https://reqres.in/api/users?page=2

https://reqres.in/api/users/2

http://universities.hipolabs.com/search?country=United+States

https://itunes.apple.com/search?term=michael&limit=1000

https://www.boredapi.com/api/activity

https://techcrunch.com/wp-json/wp/v2/posts?per_page=100&context=embed

CURL

Install curl (Client URL)

curl is a CLI application available for all OS.

https://curl.se/windows/

brew install curl

Usage

curl https://api.zippopotam.us/us/08028

curl https://api.zippopotam.us/us/08028 -o zipdata.json

Browser based

https://httpie.io/app

VS Code based

https://marketplace.visualstudio.com/items?itemName=rangav.vscode-thunder-client

Summary

Definition: REST (Representational State Transfer) API is a set of guidelines for building web services. A RESTful API is an API that adheres to these guidelines and allows for interaction with RESTful web services.

How It Works: REST uses standard HTTP methods like GET, POST, PUT, DELETE, etc. It is stateless, meaning each request from a client to a server must contain all the information needed to understand and complete the request.

Data Format: REST APIs typically exchange data in JSON or XML format.

Purpose: REST APIs are designed to be a simple and standardized way for systems to communicate over the web. They enable the backend services to communicate with front-end applications (like SPAs) or other services.

Use Cases: REST APIs are used in web services, mobile applications, and IoT (Internet of Things) applications for various purposes like fetching data, sending commands, and more.

Demo

https://github.com/gchandra10/rust-rest-api-demo

CICD

A CI/CD Pipeline is simply a development practice. It tries to answer this one question: How can we ship quality features to our production environment faster?

img src

Without the CI/CD Pipeline, the developer will manually perform each step in the diagram above. To build the source code, someone on your team has to run the command to initiate the build process manually.

Continuous Integration (CI)

Automatically tests code changes in a shared repository. Ensures that new code changes don't break the existing code.

Continuous Delivery (CD)

Automatically deploys all code changes to a testing or staging environment after the build stage, then manually deploys them to production.

Continuous Deployment

This happens when an update in the UAT environment is automatically deployed to the production environment as an official release.

CICD Tools

On-Prem & Web

  • Jenkins
  • Circle CI

Web Based

  • Github Actions
  • GitLab

Cloud Providers

  • AWS CodeBuild
  • Azure DevOps
  • Google Cloud Build

GitHub Actions

  • Workflows
  • Jobs
  • Events
  • Actions
  • Runners

Runners - Remote computer that GitHub Actions uses to execute the jobs.

Github-Hosted Runners - ubuntu-latest - windows-latest - macos-latest

Actions - Reusable commands that can be used in your config file.

https://github.com/features/actions

Events - Trigger the execution of the job.

On Push / Pull On Schedule On workflow_dispatch (Manual Trigger)

Jobs - Tasks GitHub Action to execute.

It consists of steps that GitHub Actions will execute on a runner.

Workflows - Automated processes that contain one or more logical jobs. Entire to-do list.


YAML (Yet Another Markup Language)

YAML is a human-friendly data serialization language for all programming languages.

https://learnxinyminutes.com/docs/yaml/

Create a folder .github/workflows and copy the workflow YAML file inside that folder.

Multiple Runners DEMO

https://github.com/gchandra10/github-actions-multiple-runners-demo

Rust DEMO

https://github.com/gchandra10/rust-ci-demo

Sample

name: Rust

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build_step:
    runs-on: ubuntu-latest

    steps:
    - name: Discord - Process started
      env:
        DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
      uses: Ilshidur/action-discord@master
      with:
        args: ' :information_source: The Calculator App {{ EVENT_PAYLOAD.repository.full_name }} workflow (${{ github.run_id }}) was triggered by ${{ github.actor }}'

    - uses: actions/checkout@v2
    - name: Install Rust
      uses: actions-rs/toolchain@v1
      with:
        toolchain: stable
        override: true

    - name: Build
      uses: actions-rs/cargo@v1
      with:
        command: build
        args: --release

    - name: Run tests
      uses: actions-rs/cargo@v1
      with:
        command: test

    # - name: Format the Code
    #   uses: actions-rs/cargo@v1
    #   with:
    #     command: fmt

    - name: Send Discord Failure Notification
      # https://github.com/marketplace/actions/actions-for-discord
      if: failure()
      env:
        DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
      uses: Ilshidur/action-discord@master
      with:
        args: '@here :x: The Calculator App integration {{ EVENT_PAYLOAD.repository.full_name }} test failed. Check the Run id ${{ github.run_id }} on Github for details.'

    - name: Send Discord Success Notification
      # https://github.com/marketplace/actions/actions-for-discord
      if: success()
      env:
        DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
      uses: Ilshidur/action-discord@master
      with:
        args: ' :white_check_mark: The Calculator App {{ EVENT_PAYLOAD.repository.full_name }} - ${{ github.run_id }} successfully integrated and tested.'

    # - name: Generate Documentation
    #   uses: actions-rs/cargo@v1
    #   with:
    #     command: doc

    ## Not that popular these days.
    # - name: Send Email Notification on Failure
    #   if: failure()
    #   uses: dawidd6/action-send-mail@v2
    #   with:
    #     server_address: ${{ secrets.EMAIL_SERVER }}
    #     server_port: ${{ secrets.EMAIL_PORT }}
    #     username: ${{ secrets.EMAIL_USERNAME }}
    #     password: ${{ secrets.EMAIL_PASSWORD }}
    #     subject: CI Failure in ${{ github.repository }}
    #     to: chandr34@rowan.edu
    #     from: chandr34@rowan.edu
    #     body: The Rust CI test failed. Check the details on GitHub.
    

How to use Multiple .rs files

first.rs

fn first(){
    println!("{}","From first function");

}
fn main() {
    println!("Hello from first main file!");
    first();
}

second.rs

fn second(){
    println!("{}","From second function");

}

fn main() {
    println!("Hello from second main file!");
    second();
}
cargo run --bin first

cargo run --bin second

Storage Format

Account numberLast nameFirst namePurchase (in dollars)
1001GreenRachel20.12
1002GellerRoss12.25
1003BingChandler45.25

Row Oriented Storage

In a row-oriented DBMS, the data would be stored as

**1001,Green,Rachel,20.12;**1002,Geller,Ross,12.25;1003,Bing,Chandler,45.25

Best suited for OLTP - Transaction data.

Columnar Oriented Storage

1001,1002,1003;Green,Geller,Bing;Rachel,Ross,Chandler;20.12,12.25,45.25

Best suited for OLAP - Analytical data.

Compression: Since the data in a column tends to be of the same type (e.g., all integers, all strings), and often similar values, it can be compressed much more effectively than row-based data.

Query Performance: Queries that only access a subset of columns can read just the data they need, reducing disk I/O and significantly speeding up query execution.

Analytic Processing: Columnar storage is well-suited for analytical queries and data warehousing, which often involve complex calculations over large amounts of data. Since these queries often only affect a subset of the columns in a table, columnar storage can lead to significant performance improvements.

Img Src: https://mariadb.com/resources/blog/why-is-columnstore-important/


CSV/TSV/Parquet

  • Comma Separated Values
  • Tab Separated Values

**Pros

  • Tabular Row storage.
  • Human-readable is easy to edit manually.
  • Simple schema.
  • Easy to implement and parse the file(s).

Cons

  • There is no standard way to present binary data.
  • No complex data types.
  • Large in size.

Parquet

Parquet is a columnar storage file format optimized for use with Apache Hadoop and related big data processing frameworks. Twitter and Cloudera developed it to provide a compact and efficient way of storing large, flat datasets.

Best for WORM (Write Once Read Many)

The key features of Parquet are:

Columnar Storage: Parquet is optimized for columnar storage, unlike row-based files like CSV or TSV. This allows it to compress and encode data efficiently, making it a good fit for storing data frames.

Schema Evolution: Parquet supports complex nested data structures, and the schema can be modified over time. This provides much flexibility when dealing with data that may evolve.

Compression and Encoding: Parquet allows for highly efficient compression and encoding schemes. This is because columnar storage makes better compression and encoding schemes possible, which can lead to significant storage savings.

Language Agnostic: Parquet is built from the ground up for use in many languages. Official libraries are available for reading and writing Parquet files in many languages, including Java, C++, Python, and more.

Integration: Parquet is designed to integrate well with various big data frameworks. It has deep support in Apache Hadoop, Apache Spark, and Apache Hive and works well with other data processing frameworks.

In short, Parquet is a powerful tool in the big data ecosystem due to its efficiency, flexibility, and compatibility with a wide range of tools and languages.

CSV vs Parquet

MetricCSVParquet
File Size~1 GB100-300 MB
Read SpeedSlowerFaster for columnar ops
Write SpeedFasterSlower due to compression
Schema SupportNoneStrong with metadata
Data TypesBasicWide range
Query PerformanceSlowerFaster
CompatibilityUniversalRequires specific tools
Use CasesSimple data exchangeLarge-scale data processing

These metrics highlight the advantages of using Parquet for efficiency and performance, especially in big data scenarios, while CSV remains useful for simplicity and compatibility.

Apache Arrow (https://arrow.apache.org/)

Apache Arrow defines a language-independent columnar memory format for flat and hierarchical data, organized for efficient analytic operations on modern hardware like CPUs and GPUs. The Arrow memory format also supports zero-copy reads for lightning-fast data access without serialization overhead.

While Parquet is a storage format and Arrow is an in-memory format, they are often used together. Data stored in Parquet files can be read into Arrow’s in-memory format for processing, and vice versa.

Both formats are maintained by the Apache Software Foundation, and they are designed to complement each other. Arrow provides a standard in-memory format, while Parquet provides a standard on-disk format. Together, they enable efficient data processing workflows that involve both storage and in-memory analytics.

Polars

Polars is a high-performance DataFrame library designed for Rust and Python, aiming to provide fast data manipulation capabilities similar to those found in libraries like Pandas for Python.

  • Performance: Polars is built for speed, leveraging Rust’s performance capabilities.

  • Lazy Execution: Polars supports lazy execution, allowing you to build complex query plans that are only executed when needed. This can optimize performance by minimizing unnecessary computations.

  • Expressive API: Polars offers an expressive and flexible API for data manipulation, including support for operations like filtering, aggregation, joining, and more.

  • Interoperability: While Polars is native to Rust, it also has a Python API, making it accessible to a broader range of developers.

Sure, here's a tabular comparison of Polars and Pandas:

FeaturePolarsPandas
LanguageRust, with Python bindingsPython
PerformanceHigh performance due to parallel execution and memory efficiencyGenerally slower for large datasets, single-threaded execution
Memory UsageMore memory efficientHigher memory usage
Lazy ExecutionYes, supports lazy evaluation and query optimizationNo, operations are immediately executed
APIExpressive and composable API, consistent behaviorMature and versatile API, some inconsistencies
Type SafetyStrong type safety due to RustDynamically typed
Memory SafetyEnsured by Rust's ownership modelRelies on Python's garbage collector
ScalabilityBetter for large datasets and complex operationsCan struggle with very large datasets
InteroperabilitySupports Rust and Python, integrates with Apache ArrowPrimarily Python, integrates with many Python libraries
GroupBy OperationsFast and efficient, especially on large datasetsSlower, can be memory intensive
Handling Large DataCan handle larger-than-memory datasets more efficientlyLimited by memory size
MultithreadingYes, utilizes multiple CPU coresNo, single-threaded execution
Data RepresentationUses Apache Arrow formatNative Pandas data structures
RobustnessVery robust due to Rust's type and memory safetyRobust, but can have runtime errors
Ease of UseRequires familiarity with Rust concepts for advanced useEasy to use, especially for those familiar with Python
Community and EcosystemGrowing community, less extensive ecosystem compared to PandasLarge community, extensive ecosystem and support

Conclusion

  • Polars is ideal for high-performance requirements, handling large datasets, and applications where memory efficiency and parallel execution are critical. It benefits from Rust's safety features and offers a powerful, composable API.
  • Pandas is great for general data manipulation and analysis tasks, with a mature and versatile API, extensive ecosystem, and ease of use, particularly for Python developers.

Choosing between Polars and Pandas depends on your specific needs, including performance requirements, dataset size, and preferred development language.

Demo

https://github.com/gchandra10/rust-polars-csv-dataframe-demo

Convert CSV to Parquet

https://crates.io/crates/csv2parquet

cargo install csv2parquet

csv2parquet sales_100.csv sales_100.parquet

use parquet::file::reader::{FileReader, SerializedFileReader};
use std::fs::File;
use std::path::Path;

fn main() {
    let file = File::open(&Path::new("sales_100.parquet")).unwrap();
    let reader = SerializedFileReader::new(file).unwrap();
    let mut iter = reader.get_row_iter(None).unwrap();

    // Retrieve the schema and column names
    let schema = reader.metadata().file_metadata().schema_descr();
    let columns: Vec<String> = schema.columns().iter().map(|c| c.name().to_string()).collect();

     // Print the header
     println!("{}", columns.join(","));

    while let Some(record) = iter.next() {
        println!("{:?}", record);
    }

}

RHAI

Rhai is an embedded scripting language for Rust that allows you to extend your Rust applications with scripting capabilities.

It is highly extensible and safe, making it suitable for various applications where scripting capabilities are desired.

RHAI Playground

RHAI Scripts

RHAI Book

Demo to see capability of RHAI before learning the use cases.

Examples:

git clone https://github.com/gchandra10/rust-rhai-demo.git

How Rhai Works

Rhai scripts are written in a simple, easy-to-learn syntax. When you execute a Rhai script, it goes through several stages:

Parsing: The script is parsed into an Abstract Syntax Tree (AST). When this script is parsed by Rhai, it gets converted into an AST. The AST is a tree-like structure that represents the syntactic structure of the script.

Compilation: The AST is compiled into an internal representation.

Execution: The compiled script is executed by the Rhai engine.

Check AST in Rhai Playground

let x = 5;
let y = x * 2;
print(y);

Why Use Rhai?

Ease of Embedding: Rhai is designed to be easily embedded within Rust applications, providing a simple API for integration.

Safety: Rhai ensures safety through Rust's ownership and borrowing principles, making it a reliable choice for applications where security is critical.

Extensibility: Rhai allows you to extend its functionality with Rust code, enabling you to create custom functions and integrate seamlessly with existing Rust libraries.

Performance: Although it is an interpreted language, Rhai is designed to be fast and efficient, suitable for performance-critical applications.

Simplicity: The syntax of Rhai is straightforward and easy to learn, making it accessible to both developers and end-users who need to write scripts.

Concurrency: Rhai supports concurrency, which can be beneficial for applications that require multitasking or parallel execution.

Dynamic Typing: Rhai is dynamically typed, allowing for flexible and expressive scripting without the need for explicit type declarations.

Use Cases

Game Development: Embedding Rhai to script game logic, AI behavior, or user interfaces.

Configuration: Allowing users to define configuration scripts that can modify the behavior of an application at runtime.

Automation: Providing a scripting interface for automating tasks or extending the functionality of an application.

Prototyping: Quickly testing and iterating on new features without recompiling the entire Rust application.

Why not use RHAI

Its not as fast native Rust.

Doesn't support Classes/Traits/Closures/Tuples

Concurrency & Parallelism

Concurrency and parallelism are both about handling multiple tasks at the same time, but they approach the problem differently.

Concurrency

Definition: Concurrency is the ability of a program to manage multiple tasks at once. It doesn't necessarily mean these tasks are running at the same exact time, but that the system can handle multiple tasks in progress.

Example: Think of it as a chef preparing several dishes at once. The chef switches between tasks, chopping vegetables, stirring a pot, and checking the oven.

Parallelism

Definition: Parallelism is about executing multiple tasks simultaneously, typically on multiple CPU cores. This is a subset of concurrency where tasks are literally running at the same time.

Example: Imagine multiple chefs in a kitchen, each preparing a different dish at the same time.

How It Works in Rust

Concurrency in Rust

Rust achieves concurrency through threads and async/await.

Threads: Rust's standard library provides native support for threads, which are the basic unit of concurrency.

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        // This is the code that will run in a new thread.
        for i in 1..10 {
            println!("Hello from the spawned thread! {}", i);
        }
    });

    // Meanwhile, the main thread continues.
    for i in 1..5 {
        println!("Hello from the main thread! {}", i);
    }

    // Wait for the spawned thread to finish.
    handle.join().unwrap();
}

Async/Await: Rust's async/await syntax allows you to write asynchronous code that looks like synchronous code.

use tokio; // Using the Tokio async runtime

#[tokio::main]
async fn main() {
    let future1 = async_task();
    let future2 = async_task();

    // `join!` runs multiple futures concurrently
    tokio::join!(future1, future2);
}

async fn async_task() {
    // Some asynchronous work
    println!("Doing async work");
}

Parallelism in Rust

Rust achieves parallelism primarily through the Rayon library, which provides data parallelism through easy-to-use abstractions.

Rayon: Rayon makes it easy to parallelize data processing tasks.

Key Points

Safety: Rust ensures thread safety and data race prevention through its ownership and borrowing system.

Ease of Use: Libraries like Rayon and Tokio make it easier to work with concurrency and parallelism without delving too deep into low-level details.

git clone https://github.com/gchandra10/rust-concurrency-parallelism.git

References