Rust 101 — Everything you need to know about Rust

Introduction to Rust Programming — the most loved programming language

Nishant Aanjaney Jalan
CodeX
9 min readFeb 25, 2023

--

Since 2014, I have learnt quite a few languages, mainly focusing on Java, Kotlin and JavaScript. Although I recently learnt Haskell for university purposes, I wanted to learn a new language that has a better overall reputation. A few weeks ago, I turned to Rust, and it has been really good so far. In this article, I am going to share the basics of Rust so that you can start programming today.

Source

Why Rust?

Rust is a programming language that is built to be fast, secure, reliable and supposedly easier to program. Rust was, on the contrary, not built to replace C or C++. It’s aim is completely different, that is, to be an all-purpose general programming language.

Features of Rust

  1. Code should be able to run without crashing first time — Yes, you heard me. Rust is a statically typed language, and based on the way it is constructed, it leaves almost (I say, almost!) no room for your program to crash.
  2. Super fast — Rust is extremely fast when it comes to compilation and execution time.
  3. Zero-cost abstractions — All Rust code is compiled into Assembly using the LLVM back-end (also used by C). Programming features like Generics and Collections do not come with runtime cost; it will only be slower to compile. (Source: stackoverflow).
  4. Builds optimized code for Generics — If you have generic code, the Rust’s compiler can smartly identify the code for, say, Lists and Sets, and optimize it differently for both.
  5. Rust Compilation Errors — Rust’s compiler gives the most helpful error messages if you mess up your code. In some cases, the compiler will give you code that you can copy/paste in order for it to work.
  6. Documentation — Rust has an amazing Documentation, and a tutorial guide called The Rust Book. The Rust Book is your best friend in this journey.

Now let us get into actually programming in Rust.

Installing Rust On Your System

The Rust Book explains how you can install Rust on your system.

Since I use Linux, it is a simple command that installs Rust without any hassle,

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

A Brief tour of Rust

Variables

By default, all variables are immutable unless we mark them. Rust is statically typed, so if a variable is declared to be mutable, we must be careful to assign it of the same type.

Defining variables are simple:

fn main() {
let myName = "Nishant"; // Rust infers type to be String
let coins: u32 = 4; // explicit type, unsigned 32-bit integer

let age = 19; // number defaults to i32 type, signed 32-bit integer
}

If we declare a variable, and we try to use it without initializing, the Rust compiler will complain.

// * WILL NOT COMPILE
fn main() {
let coins: u32;
foobar(coins);
}

fn foobar(num: u32) {
println!("The number sent was {}", num);
}
Compiling hello_cargo v0.1.0 (/home/nishant/Programming/Rust/hello_cargo)
error[E0381]: used binding `coins` isn't initialized
--> src/main.rs:3:9
|
2 | let coins: u32;
| ----- binding declared here but left uninitialized
3 | foobar(coins);
| ^^^^^ `coins` used here but it isn't initialized
|
help: consider assigning a value
|
2 | let coins: u32 = 0;
| +++

For more information about this error, try `rustc --explain E0381`.
error: could not compile `hello_cargo` due to previous error

As Rust programmers, we must read all error messages fully. The error message here tells us that we have used coins but we haven’t initialized it. The message goes on to say that in line 2, we should append = 0 in order for the code to work!

There are many data types used in Rust. We have come across String, i32 and u32. We also have,

fn main() {
let temperature: f32 = 6.4;
let circumference: f64 = 23053.7106;

let grade: char = 'A';
let pass: bool = true;
}

Those above were some more primitive data types. Rust has support for compound data types as well!

fn main() {
let pair = ('A', 65);

println!(pair.0) // Accessing first element
println!(pair.1) // Accessing second element

// Destructuring a pair.
let (letter, number) = pair
}

The implicit type for pair is (char, i32). Tuples are heterogeneous and can support nested tuples as well.

Additionally, we can work with Arrays as well,

fn main() {
let a = [1, 2, 3, 4, 5];
// a has a type [i32; 5] - an array of five signed 32-bit integers.
}

A data type declaration can hint towards a quick way to initialize arrays.

fn main() {
let a = [3; 5];

for i in a {
println!("{i}");
}
}

// This program will print 3 on five lines.

Functions

We have seen we have been using the main function to denote the starting point in our program. The syntax of defining functions is

fn <function-name>(<param-name>: <param-type>) -> <return-type> {
body
}

An example function can be like:

fn is_divisible(num: i32, dividend: i32) -> bool {
num % dividend == 0
}

Notice I do not have a semicolon at the end of that statement. This signifies that the expression will return a particular value. If I add a semicolon, Rust will treat the expression as a statement and will complain I am not returning a boolean value.

Let Expressions

Combining the knowledge of variables and functions, we can assign values like this:

let x = {
let y = 1;
let z = 2;

y + z // Note the lack of semicolon to indicate return value
}

Hence, we can conclude that:

fn main() {
let x = 0;
let x = { 0 }; // these two are the same!
}

Variable Shadowing and Scopes

Notice how I didn’t prefix the previous code block with // * WILL NOT COMPILE. However, I do have two declarations of the same variable. This is called variable shadowing.

fn main() {
let x = 0;
let x = { 10 }; // shadowed the previous value of x
}

First we initialize x to be 0, and then I am re-initializing it to be 10. This is a valid program and useful in many ways when we couple it with scopes!

Scopes are just a block of code where shadowed variables do not affect the value of the variable outside the scope.

fn main () {
let x = 4;

{
let x = "shadowing x";
println!("{}", x); // pfints "shadowing x"
}

println!("{}", x); // prints "4"
}

Namespaces

If we wish to use functions from other libraries, we can use namespaces.

fn main() {
let least = std::cmp::min(3, 8);

println!("{}", least);
}

We can also bring the function into scope by using the use keyword.

use std::cmp::min;

fn main() {
let least = min(3, 8);

println!("{}", least);
}

We can also use std::cmp::* to bring every function inside std::cmp into scope.

Structs

Structs are similar to structs in C. We can define a struct Coordinate as below and initialize a variable of that type.

struct Coordinate {
x: f64,
y: f64
}

fn main() {
let somewhere = Coordinate { x: 23, y: 3.5 };

// Spreading the values of somewhere and updating x to 5.4
// make sure that ..somewhere is at the end.
let elsewhere = Coordinate { x: 5.4, ..somwhere };

// Destructuring Coordinate.
let Coordinate {
x,
y
} = elsewhere;
}

We can also implement our functions to structs that we define.

impl Coordinate {
fn add(self, coord: Coordinate) -> Coordinate {
let newX = self.x + coord.x;
let newY = self.y + coord.y;

Coordinate { x: newX, y: newY }
}
}

Pattern Matching

Pattern Matching is like a conditional structure.

fn main() {
let coordinate = Coordinate { x: 3.0, y: 5.0 }

if let Coordinate { x: 3.0, y } = coordinate {
println!("(3, {})", y);
} else {
println!("x != three");
}
}

We can also perform pattern matching using the match construct.

fn main() {
let coordinate = Coordinate { x: 3.0, y: 5.0 }

match coordinate {
Coordinate { x: 3.0, y } => println!("(3, {})", y);
_ => println!("x != three");
}
}

Alternatively, this code also compiles:

fn main() {
let coordinate = Coordinate { x: 3.0, y: 5.0 }

match coordinate {
Coordinate { x: 3.0, y } => println!("(3, {})", y);
Coordinate { .. } => println!("x != three");
}
}

.. here means ignore the (remaining) properties inside the Coordinate struct.

Traits

Traits are like type classes in Haskell, or interfaces in Java. Say that we have a struct, Number which looks like this:

struct Number {
value: isize,
prime: bool
}

We could define a trait Parity that contains a function is_even. If we implement Parity for Number, we need to define the trait’s functions.

trait Parity {
fn is_even(self) -> bool
}

impl Parity for Number {
fn is_even(self) -> bool {
self.value % 2 == 0
}
}

We can also implement traits for foreign types as well!

// Using our struct for foreign type
impl Parity for i32 {
fn is_even(self) -> bool {
self % 2 == 0
}
}

// Using foriegn trait for our struct
impl std::ops:Neg for Number {
type Output = Number;

fn neg(self) -> Self::Output {
Number {
value: -self.value
..self
}
}
}

But what we cannot do is define foreign traits for foreign structs

// * WILL NOT COMPILE 
impl<T> std::ops::Neg for Vec<T> {
type Output = isize;

fn neg(self) -> Self::Output {
-self.len()
}
}

Macros

Macros are a part of meta-programming. Marcos can be considered as little programs that other programs can use. All macros end with ! and can be defined as either of the following:

macro_name!()
macro_name!{}
macro_name![]

println! is a macro that uses std::io to write something to the console. Similarly, vec![] defines a vector, which is an array with steroids.

fn main() {
let vec1 = vec![1,2,3];

for number in vec1 {
println!("{number}");
}
}

panic! is also a macro that terminates a program (actually a Thread, if you know about concurrency) with an error message. panic! is one of the only places in our program that can crash.

Enums

Enums in Rust are like sealed class in Kotlin. They are certain types that we can define in an enclosure. The following example is coupled with Generics. Option is also defined in the standard library.

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

impl Option<T> {
unwrap() -> T {
match self {
Self::None -> panic!("unwrap called on an Option None"),
Self::Some -> T,
}
}
}

The Result enum

The most popular enum Rust provides us with is Result.

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

If we have a function that returns a result, we can safely handle it.

fn main() {
{
let result = do_something(); // returns result

match result {
Ok(data) => proceed(data),
Err(error) => panic!("There has been an error!"),
}

// continue program execution
}
}

The reason I have used this code within a scope block is if we wish to propagate the error up somewhere, we could replace the code with:

fn main() {
{
let result = do_something(); // returns result

match result {
Ok(data) => proceed(data),
Err(error) => return error,
}

// continue program execution
}
}

There is a shorthand to do this operation:

fn main() {
{
let data = do_something()?; // returns result

// work with data directly
}
}

Now if result returns an Ok(data), then the question mark will return the data object to us.

This is most of what you would require to start basic programming in Rust. I hope you are able to see how Rust is useful and a powerful language. I am still learning the basics of Rust and would post more articles on Rust in the future.

This article is part of my Rust Programming List:

Rust Programming

If you wish to read every article from me, consider joining the Medium 
program with this referral link.

Want to connect?

My GitHub profile.
My Portfolio website.

--

--

Nishant Aanjaney Jalan
CodeX
Editor for

Undergraduate Student | CS and Math Teacher | Android & Full-Stack Developer | Oracle Certified Java Programmer | https://cybercoder-naj.github.io