Day 1/100: An Overview of Rust

Day 1/100: An Overview of Rust

Lessons from the Rust Book: chapters 1 & 2

Introduction

Before starting out this challenge, I first heard of rust on ThePrimeagen's stream and decided to explore what the language was about. To provide a bit of context, I am interested in working on backend and infrastructure software, as well as tools to improve developer productivity. As a terminal lover, most of the tools I focus on are CLI based. Sorry my GUI friends :).

Briefly, What is Rust?

Rust presents itself as a systems programming language that focuses on safety, speed and concurrency. Just like every software design, there is always a tradeoff for speed and correctness. The debt for safety checks and memory management for Rust happens at compile time.

The quote, "If it compiles, then it works", is mostly true for rust programs. Then again, this is subjective and semantic errors are not accounted for. Despite the build time cost, the program's runtime is not affected. So yeah, rust programs are blazingly fast :).

Some Syntax & Design Rules

My first rust program:

fn main() {
    println!("Hello world!");
}
  • The main() function is the first function that is run.

  • Rust prefers to use macros for metaprogramming. println! in the code above is a macro and not a function.

Metaprogramming is writing programs that can manipulate or generate other programs. To know why println is implemented as a macro, refer to this reddit post here.

  • Lines end with a semi-colon (;).

  • Rust is an ahead of time compiled language. That means, you can build a rust program and the distributed binary can run without the rust compiler.

Rust Toolchain: Cargo

  • Cargo is rust's build system and package manager.

  • It handles any task related to building, debugging, running and resolving dependencies in a rust project.

  • Cargo helps to create either a binary application or a library application.

Libraries are prewritten programs that other developers can use in their projects as dependencies.

  • Cargo is configured in a rust project via a configuration file called Cargo.toml.

I never knew toml was an acroymn. It stands for Tom's Obvious Minimal Language. Interesting, isn't :) ?

Sample cargo commands:

cargo run             # runs a rust project
cargo build           # builds a rust project
cargo new --lib       # creates a library project
cargo new --bin       # create a binary application project

A Taste of Rust: The Guessing Number Game

The Prelude

There are a list of packages that are essential for the basic running of a rust program. These packages are brought into the default scope. This means they can be used without explicitly specifying their 'imports'.

What will be the __main__ scope in python, is called The Prelude in rust. The reason for not bringing all standard library packages into scope to increase the 'lightness' (less weight) of your program.

Imports: Bringing Packages Into Scope

To use a package not present in the prelude, the use keyword is used to specify them.

For example, to get user input, we need the io module in the standard library:

use std::io;

Getting Input

To use the value from a user input there are 3 steps rust forces you to take:

  1. Ask for the user input.

  2. Process the user input.

  3. Check if the input is in the correct form and handle any potential errors.

Translation into rust at this stage:

io::stdin()
    .read_line(&mut guess)
    .expect("Failed to read input");

A package in rust is called a crate :). Anytime you hear crate, think of a package.

The detailed explanation of this code is in The Rust Book.

Overview of Variables

  • The let keyword is used to create variables.

  • By default, variables are immutable. That is, they cannot be changed.

  • To make a variable mutable, you need to specify that using the mut keyword.

Sample code:

let guess = String::new();                 // immutable variable
let mut secret_number = String::new();     // mutable variable

References

& indicates a reference. A reference allows you to let multiple parts of your code access one piece of data without needing to copy that data into memory multiple times. Just like variables, references are immutable by default.

&mut indicates a mutable reference.

Sample code:

println!("{}", &guess);         // immutable reference
println!("{}", &mut guess);     // mutable reference

Result Types & Error Handling

Rust have Result types which is an enum that has some value when everything is okay and an error if an operation fails. This is different from Go's if err != nil.

An enumeration/enum is a type that can have a fixed set of values. The values are known as the enum's variants.

The variants of the Result type are Ok and Err.

  • Ok holds some value if the process that returned a result is successful.

  • Err holds an error message that tells how and why a process failed.

One of the ways to handle errors is to use the match syntax (This is known as a graceful error handling).

Sample code: We want to convert our user input from a String into an integer.

let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => {
        if guess.trim() == "exit" {
            break;
        } else {
            println!("Invalid input: Please enter a number!");
            continue;
        }
    }
};

The main purpose of Result types is to encode error handling information.

The underscore used in Err(_) is known as a catchall value. This means that we want to match any kind of error that occurs.

Ungraceful error handling: You can use .expect() method on a function or method that returns a result type. .expect() will cause your program to crash and display the message that you passed as an argument to it.

Sadly, header linking is not working on hashnode :(.

Kindly refer to the sample code under Getting Input to see how ungraceful error handling is done in rust.

Thanks :)

Overview of Match Expressions and Comparison Operations

Comparison between values in rust is done via the cmp module in the standard library. It compares two values (anything that can be compared) and returns a variant of the Ordering enum.

The Ordering enum variants are self explanatory:

  • Ordering::Less

  • Ordering::Equal

  • Ordering::Greater

Comparison operations can be used with a match expression to compare a pattern with a value and a piece of code that should be run if there is a successful match.

Sample code:

match guess.cmp(&secret_number) {
    Ordering::Less => println!("Too small!"),
    Ordering::Equal => println!("You win!");
    Ordering::Greater => println!("Too big!"),
}

One of the safety features of the match expression is that the rust compiler forces you to handle all the possible cases a value could evaluate to. As a result, match expressions are exhaustive.

Other Notes

  • Cargo automatically creates and updates a Cargo.lock file to ensure reproducible builds.

  • The println! macro uses placeholders to allow the printing of variables and other data types. Placeholders are specified using curly braces ({})in the string argument.

  • cargo doc --open command allows you to see cargo's autogenerated documentation of your project source code and all external crates.

Concluding Thoughts

I am excited about this new journey in rust. While using a great tool gives you satisfaction, I have learned over the years that great tool does not mean a great product will be produced. I look forward to understand the Rust language and know when to make use of its features to solve the software engineering problems we have today and may encounter in the future. Let's see what happens.

I'll see you tomorrow :)

Resources & References