Rust Mastery Week #1
Table of contents
- Why I am Learning Rust?
- Rust Installation
- VS Code setup and extensions
- Setting up the VS Code Debugger for Rust
- First "Hello, World!" Program
- Anatomy of a Rust Program
- Compiling and Running Are Separate Steps
- Introduction to Cargo
- Variables and Mutability
- Constants
- Shadowing
- Data Types
- Type Inference in Rust
- Functions
Hi there! My name is Soumitra Saha. You can find me on LinkedIn, Twitter, and GitHub. I am a Full-Stack TypeScript
Developer with over 2 years of experience. In this blog, I will document my journey of learning Rust. I will post updates on my progress every week, either on Saturday or Sunday.
Why I am Learning Rust?
I'm working on a new side project to build a desktop application using the tauri
framework. During this process, I decided to learn Rust for its native functionality. Through my research, I discovered that Rust is incredibly fast, similar to C and C++. Since C++ was my first programming language, this motivates me even more to learn Rust. I've also heard about Rust's excellent memory management and security features. It is widely used in low-level systems, which adds to my interest.
Rust Installation
First Install the rust from this site https://www.rust-lang.org/
Then go to
Get Started
And follow the steps that are there, If you need additional help
Take help from YouTube for Rust Installation.
The following steps install the latest stable version of the Rust compiler.
Now check the version for confirmation of the installation
$ rustc --version
$ cargo --version
VS Code setup and extensions
The extension installation for VS Code is divided into three categories: Must-Have, Good to Have, and Optional.
Must-Have Extensions
rust-analyzer
(by rust-lang.org)CodeLLDB
(Vadim Chugunov) – For debugging Rust code. The setup for the debugger is in the next section.
Good to Have Extensions
Better TOML
(orEven Better TOML
)Crates
(Seray Uzgur)
Optional Extensions
Error Lens
Tabnine
– My preference for AI assistance while writing code. It's free to use.Code Runner
(Jun Han) - 26,530,769+ Downloads
Setting up the VS Code Debugger for Rust
CodeLLDB
is must-have extension for debugging Rust program in vscode
In many cases, VS Code can automatically configure the debugger for your Rust program without additional setup. Follow these steps to verify if automatic debugging is enabled:
Write a simple "Hello, World!" program (see the next section for the code).
Set a breakpoint in your code by clicking to the left of the line number in the editor.
Click the
Run
tab on the top bar.Select
Start Debugging
or pressF5
.
If the debugger starts successfully, you're all set! If not, follow the manual setup process below.
Manual Debugging Setup
If automatic setup does not work, you can manually configure the debugger:
Click the
Run
tab on the top bar.Select
Run Without Debugging
or pressCtrl + F5
.Choose
LLDB
from the list of debugger types.You might see some warnings. Click
Yes
->Yes
to proceed.VS Code will create a
launch.json
file in the.vscode
folder in your current working directory.
For a visual guide, you can refer to this video: How to Debug Rust with VS Code
First "Hello, World!" Program
Let's start with a simple "Hello, World!" program in Rust.
Step 1: Initiate a Cargo Project
Cargo is Rust's package manager and build system. It simplifies the process of managing dependencies and building your projects, similar to go mod
in Go or npm init
in Node.js.
To create a new Cargo project, run the following commands in your terminal:
$ cargo new rust_learning_project
$ cd rust_learning_project
This command will create a directory structure like this:
|-- src
| |-- main.rs
|-- .gitignore
|-- Cargo.lock
|-- Cargo.toml
Step 2: Write the "Hello, World!" Code
Open the src/
main.rs
file and add the following code:
fn main() {
println!("Hello, World!");
}
Step 3: Run the Code
To compile and run your program, use the following command:
$ cargo run
You should see the output:
Hello, World!
Alternative: Using an Online Rust Compiler
For a quick run without setting up a local environment, you can use an online Rust compiler like OnlineGDB.
Or, for running a single .rs
file you can use
$ rustc main.rs
// $ rust <file_name>
Anatomy of a Rust Program
In Rust, functions are defined using the fn
keyword. The main
function is special because it's the entry point of every executable Rust program. Here’s a simple example:
fn main() {
println!("Hello, world!");
}
Function Declaration: The
main
function is declared withfn main()
. It has no parameters and returns nothing. If there were parameters, they would go inside the parentheses()
.Function Body: The body of the function is enclosed in curly brackets
{}
. Rust requires these around all function bodies. It's a good practice to place the opening curly bracket on the same line as the function declaration, with one space in between.Automatic Formatting: Rust has an automatic formatting tool called
rustfmt
. It helps maintain a consistent coding style. You can check the online documentation to see if it's included in your Rust installation.
Inside the main
function, we have the following line:
println!("Hello, world!");
Indentation: Rust style is to indent with four spaces, not a tab.
Macros:
println!
is a macro, not a function. This is indicated by the exclamation mark!
. If it were a function, it would be written asprintln
without the!
. We will discuss Rust macros in more detail in a later section.String Argument: The
"Hello, world!"
string is passed as an argument to, which prints it to the screen.Semicolons: Lines of Rust code usually end with a semicolon
;
, indicating the end of an expression.
Compiling and Running Are Separate Steps
In Rust, compiling and running are separate steps, each serving a distinct purpose. Let's break down the process:
Compilation: Before executing a Rust program, it must be compiled using the Rust compiler (
rustc
). For instance:$ rustc main.rs
This command compiles
main.rs
and generates a binary executable.Executable Output: After successful compilation, Rust outputs a binary executable. You can view it using the shell's file listing command, such as
ls
on Unix-like systems:$ ls main main.rs
On Windows, it may appear as
main.exe
.Alternatively, on Windows CMD:
> dir /B main.exe main.pdb main.rs
Execution: To run the executable, use the appropriate command:
$ ./main # Unix-like systems
or
> .\main.exe # Windows
This executes the program, producing the desired output.
If
main.rs
contained a "Hello, world!" program, executing it would print the message to the terminal.
For those accustomed to dynamic languages like Javascript or Python, the concept of compiling and running separately might seem unfamiliar. Unlike such languages, Rust is ahead-of-time compiled, enabling distribution of executables without requiring the recipient to have Rust installed.
While rustc
suffices for basic programs, managing complex projects warrants a more sophisticated approach.
Introduction to Cargo
Cargo is Rust's package manager and build system, streamlining the development process by automating tasks such as dependency management, compilation, and project initialization.
Key Features:
Dependency Management: Cargo simplifies dependency management by automatically fetching and managing libraries from the crates.io repository. Dependencies are specified in the
Cargo.toml
file.Build System: It handles compilation, building, and testing of Rust projects. Cargo automatically compiles and links project dependencies, ensuring a smooth development workflow.
Project Initialization: Cargo facilitates project setup with the
cargo new
command. This initializes a new Rust project with the necessary directory structure and configuration files.
Example
Creating a New Project:
$ cargo new my_project
This command creates a new Rust project named
my_project
with the required directory structure.Building and Running:
$ cd my_project $ cargo build # Compiles the project $ cargo run # Builds and executes the project
Adding Dependencies: To include external dependencies, specify them in the
Cargo.toml
file under the[dependencies]
section. For example:[dependencies] rand = "0.8.4" # Adds the 'rand' library as a dependency
Updating Dependencies:
$ cargo update # Updates project dependencies to their latest versions
Cargo's intuitive interface and robust features make it an indispensable tool for Rust developers, enabling efficient project management and streamlined workflow.
There could be a full separate blog on Cargo. I am planning to write a comprehensive article on Cargo for my next blog post before my next week's update. Later, I will attach the blog link below. This will provide a detailed understanding of Cargo.
Variables and Mutability
In Rust, variables are immutable by default, meaning once assigned, their value cannot be changed. To make a variable mutable, use the mut
keyword. For example:
let x = 5; // Immutable variable
let mut y = 10; // Mutable variable
y = 15; // Allowed
Constants
Constants in Rust, unlike variables, are always immutable and declared using the const
keyword. They require type annotations and can be declared in any scope. Unlike variables, constants must be set to a value that can be determined at compile time, not computed at runtime.
Naming conventions suggest using all uppercase with underscores between words. For instance,
const HOURS_IN_SECONDS = 60 * 60
const PI = 3.14
Rust's compiler can evaluate certain operations at compile time, aiding readability and verification.
Constants remain valid throughout the program's execution, making them useful for universally applicable values, such as maximum player scores or physical constants.
Shadowing
When we declare a new variable with the same name as a previous one, the new variable overshadows the old one. We can shadow a variable by using the same name and repeating the let
keyword. For example:
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}
This program outputs:
The value of x in the inner scope is: 12
The value of x is: 6
Shadowing differs from marking a variable as mutable because attempting to reassign a shadowed variable without using let
results in a compile-time error. Additionally, shadowing allows us to change the type of a variable, enabling reuse of the same name for different types. For example:
let spaces = " ";
let spaces = spaces.len();
In contrast, attempting to achieve the same with mutability would lead to a compile-time error due to type mismatch. This understanding of variables sets the stage for exploring various data types in Rust.
Data Types
In Rust, every value is associated with a specific data type, which informs the compiler how to handle that value. There are two main subsets of data types in Rust: scalar and compound.
Scalar Types
Scalar types represent single values and include integers, floating-point numbers, Booleans, and characters. Let's explore these types:
Integer Types
Integers are whole numbers without a fractional component. Rust provides various integer types, both signed and unsigned, ranging from 8 to 128 bits in size.
The choice of type depends on the range of values you need to represent.
Example:
let signed:i32 = 42; // Signed Integer Type
let unsigned: u32 = 200; // Un Signed Integer Type
The integer type
i32
has two parts to it:i
- specifies signed integer type (can store both positive or negative value)32
- size of the data type (takes 32 bits of space in memory)
Categories of Integer Data Types in Rust
Depending on the size of data, we can further classify the signed and unsigned integer type into various categories:
Size | Signed | Unsigned |
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
Floating-Point Types
Floating-point numbers are numbers with decimal points, represented by
f32
andf64
in Rust.f32
is a single-precision float, whilef64
offers double precision and is the default type due to its higher precision.
Here, the
f
character represents a floating point number,32
and64
represent the size in bits.
Boolean Type
Booleans in Rust, denoted by
bool
, can have values oftrue
orfalse
.They are one byte in size and are primarily used in conditional expressions.
Character Type
The
char
type represents single Unicode scalar values, allowing Rust to handle a wide range of characters, including emoji and accented letters.Each
char
is four bytes in size.
Here,
char
represents the character type variable and we use single quotes to represent a character.
Example
fn main() {
let letter: char = 'A';
let emoji: char = '😊';
let chinese_char: char = '中';
let accented_char: char = 'é';
println!("Letter: {}", letter);
println!("Emoji: {}", emoji);
println!("Chinese Character: {}", chinese_char);
println!("Accented Character: {}", accented_char);
}
Compound Types
Compound types group multiple values into one type. Rust offers tuples and arrays as primitive compound types:
Tuple Type
Tuples can hold multiple values of different types within a fixed-length structure.
They are created by enclosing comma-separated values within parentheses.
Array Type
Arrays store multiple values of the same type in a fixed-length sequence.
They are useful when you need a fixed number of elements with the same type.
Arrays are specified by enclosing values within square brackets.
Example
fn main() {
// Scalar Types
let integer: i32 = 42;
let float: f64 = 3.14;
let boolean: bool = true;
let character: char = 'A';
println!("Integer: {}", integer);
println!("Float: {}", float);
println!("Boolean: {}", boolean);
println!("Character: {}", character);
// Compound Types
let tuple: (i32, f64, bool) = (42, 3.14, true);
let array: [i32; 5] = [1, 2, 3, 4, 5];
println!("Tuple: {:?}", tuple);
println!("Array: {:?}", array);
}
Type Inference in Rust
In Rust, we can create variables without explicitly specifying their data types. The compiler infers the data type based on the value assigned to the variable. For example:
let x = 51;
In this case, Rust automatically identifies the data type of variable x
as i32
(the default type for integer variables) because of the value 51
. This process is known as Type Inference.
Consider the following example:
fn main() {
let x = 51;
println!("x = {}", x);
}
Output:
x = 51
As you can see, we haven't explicitly mentioned the data type of the variable x
. Rust inferred it as i32
based on the assigned value 51
.
Functions
Functions in Rust are integral to the language's structure, serving as building blocks for program logic. The main function, commonly recognized as the entry point for Rust programs, exemplifies this role.
By employing the fn
keyword, we can declare new functions, adhering to Rust's conventional naming style of snake case
, where words are separated by underscores.
Let's delve into a simple example:
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
In this code snippet, we define a function using the fn
keyword followed by the function's name and a set of parentheses to denote its parameters, if any. The function body is enclosed within curly braces, signaling its scope. We invoke functions by specifying their names followed by parentheses.
Consider the following enhancement:
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {}", x);
}
In this version, we introduce parameters to the function another_function. Here, x is an i32
type parameter. When calling this function with an argument (5 in this case), Rust's type inference deduces the parameter's type, facilitating smooth integration.
Furthermore, Rust distinguishes between statements and expressions within function bodies.
While statements execute actions and do not return values,
expressions yield results.
fn main() {
let x = sum(5, 6);
println!("The value of x is: {x}");
}
fn sum(a:i32, b: i32) -> i32 {
a + b
}
This expression-based paradigm aligns with Rust's design philosophy, promoting clarity and efficiency in code composition.
Note that there is no
;
at the end of thea + b
expression. This is because the;
would not return the result of the calculation a+b, but would return the unit type()
. This is a common mistake for people coming from other languages. The()
unit type is the only type that can be returned from a function that does not have a return type specified; in C this would bevoid
.Lastly, functions can return values, with the return type declared after an arrow (->). The return value corresponds to the final expression in the function's body. For instance, consider the function
sum(a:i32, b:i32) -> i32
:In Rust, the
return
keyword is used to explicitly return a value from a function. However, unlike in some other programming languages, Rust functions don't require thereturn
keyword to return a value. The reason for this is Rust's expression-based nature.
fn add_one(x: i32) -> i32 {
x + 1 // This expression is implicitly returned
}
However, if you need to return early from a function or explicitly specify a return value, you can use the
return
keyword:
fn add_one(x: i32) -> i32 {
if x >= 0 {
return x + 1; // Explicit return with the return keyword
} else {
return x - 1; // Explicit return with the return keyword
}
}
Reference | Books
The Rust Programming Language (by Steve Klabnik and Carol Nichols)
Rust for Rustaceans (by Jon Gjengset)
Subscribe to my newsletter
Read articles from SOUMITRA-SAHA directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
SOUMITRA-SAHA
SOUMITRA-SAHA
About Me I am a Full-Stack Developer with approximately two years of experience in the industry. My expertise spans both front-end and back-end development, where I have worked extensively to build and maintain dynamic, responsive web applications. Technical Skills Front-End Development: HTML, CSS, JavaScript, Typescript Frameworks: React, NextJs, Remix, Vite Libraries: TailwindCSS, Bootstrap Tools: Webpack, Babel Back-End Development: Languages: Typescript, Node.js, Python, Go, Rust Frameworks: NextJS, Remix, Express.js, Django, Go (Gin), Rust Databases: PostgreSQL, MySQL, MongoDB Tools: Docker, Kubernetes DevOps: Version Control: Git, GitHub CI/CD: Jenkins, CircleCI Infrastructure as Code: Terraform Cloud Services: AWS, Lamda Other Skills: RESTful API Development Test-Driven Development (TDD), (vitest, jest, Cypress) Agile Methodologies Personal Interests I am passionate about learning new technologies and keeping up with industry trends. I enjoy exploring innovative solutions to complex problems and continuously seek opportunities to expand my expertise. In my free time, I engage with the developer community, contributing to open-source projects and participating in tech meetups.