In our workplace, we took on a challenge as front-end developers to delve into Rust and explore how we can create web applications. The initial step was familiarizing ourselves with the language’s fundamentals by studying the documentation. Upon initiating my study of Rust, I recognized similarities between Rust and JS/TS, drawing these was important for me to facilitate a more intuitive understanding. I wanted to share my learning path, writing this article outlining my exploration of Rust from the perspective of a front-end developer.
The Rust programming language was originally developed by Mozilla for Firefox, and it is also used in major companies such as Facebook, Apple, Amazon, Microsoft, and Google. Notable projects like Dropbox, npm, GitHub, and Deno leverage Rust too. Also the compiler of Next.js “Turbopack” has contributed to a remarkable 94.7% increase in speed in Next.js version 14.
Why the Rust programming language is gaining popularity can be attributed to several key factors. Firstly, Rust is a compiled language that generates efficient machine code. This characteristic ensures that applications developed with Rust deliver exceptional performance. Moreover, Rustis highly reliable due to its compiler, which effectively prevents undefined behavior that might otherwise result in unexpected outcomes or crashes.
Another one is its memory efficiency. While many languages either manage memory automatically, like JavaScript’s garbage collector, or have complete control over memory management, as in C or C++, Rust introduces a unique approach called the ownership model. (We will get back to this topic later)
In the upcoming sections of this article, we will explore essential topics of the Rust programming language from the perspective of a front-end developer. These topics include data types, variables, mutability, functions, tuples, arrays, and structs, as well as references and borrowing.
Rust Programming Language
Data types
The contrast between JavaScript and the Rust programming language primarily manifests in their approach to data types. JavaScript adopts a dynamic typing system, while Rust employs static typing. In Rust, it is a must to determine the types of all variables at compile time, a characteristic that aligns more closely with TypeScript.
Every value in Rust is associated with a specific data type, and these types are categorized into two main groups: Scalar and Compound types. In contrast, JS/TS has a small set of data types such as numbers, strings, booleans, and objects. Scalar types in Rust include integers (both signed and unsigned), floating-point numbers, booleans, and characters, while compound types comprise tuples and arrays.
Integers
A notable distinction in data types is that, unlike JS/TS, Rust provides size-specific choices for integers and floating-point numbers. This allows you to precisely regulate the amount of memory allocated for each type. Consequently, Rust stands out for its memory efficiency and high performance.
Floating numbers
Rust offers two floating point types: f32 and f64, with sizes of 32 bits and 64 bits. The default type is f64, because it provides a similar speed to f32 offering more precision. It’s important to note that all floating-point types in Rust are signed.
let x = 5.8;
Booleans
In Rust like in JS/TS, the Boolean type has two potential values: true and false. They are one byte in size, and it is denoted by bool.
let isRustReliable = true;
let isRustReliable: bool = true;
Characters
Rust’s char type is four bytes in size. It specifically represents a Unicode Scalar Value, allowing it to encompass a broader range of characters beyond ASCII. This includes accented letters, different alphabet characters, emojis, and zero-width spaces, making Rust’s char type more versatile in handling diverse character sets.
let char_type = 'a'
let char_type: char = 'A';
Tuples
A tuple groups together a variety of types into one compound type. Tuples come with a predetermined length, and once declared they are unable to expand or delete.
let origin: (i8, u8, f64) = (-5, 2, 2.2)
let (a,b,c) = origin; // destructuring in tuple
let firstElement = origin.0; // indexing
Arrays
Arrays, in contrast to tuples, require each of their elements to share the same type. Unlike arrays in JS/TS, Rust arrays have a fixed length, making it impossible to add or remove elements directly. If dynamic resizing is needed, similar to arrays in JS/TS, Vectors in Rust would likely be the suitable alternative.
let origin: [i8, 3] = [1, 2, 3];
let origin = [4; 3] // means [4,4,4]
let first = origin[0];
Variables and Mutability
In Rust, the default behavior for variables is immutability, meaning that their values cannot be changed once assigned. The let
keyword is used to declare variables, and if you want a mutable variable, you need to explicitly use mut
after let.
// Immutable variable
let x = 5;
// Mutable variable
let mut y = 10;
y = 15; // Valid because y is mutable
There are also constants too and one notable distinction between let is that you cannot use mut with constants. Constants, unlike variables, are inherently immutable. They are declared using the const keyword, and their type must be explicitly annotated.
const MULTIPLIER: u32 = 5;
Constants can be declared in any scope, including the global scope, making them valuable for values shared across different parts of the code.
Functions
In the Rust programming language, function bodies consist of a series of statements and optionally end in an expression. Statements are instructions that perform actions but do not return a value, while expressions evaluate to a resultant value.
fn main() {
let y = {
let x = 3; // statement
x + 1 // expression which evaluates the value assigned to y
};
println!("The value of y is: {y}");
}
In JavaScript, you can create functions using either function declarations or expressions. In Rust, you can use function declarations or lambda functions, known as closures, each with its own syntax and distinctive features.
JS
// Function Declaration
function add(a, b) {
return a + b;
}
// Function Expression
const subtract = function(a, b) {
return a - b;
};
Rust
// Function Declaration
fn add(a: i32, b: i32) -> i32 {
a + b
}
// Closure (Lambda Function)
let subtract = |a: i32, b: i32| -> i32 {
a - b
};
Ownership, References, and Borrowing
Ownership is a fundamental concept in the Rust programming language that establishes a set of rules governing how the language manages memory throughout the program’s execution. In JavaScript, memory management is typically handled by a garbage collector, which automatically reclaims memory that is no longer in use, relieving developers of explicit memory management responsibilities. Unlike Rust, JavaScript’s abstraction of memory details makes it well-suited for high-level development but sacrifices fine-grained control over memory allocation and deallocation.
The stack, organized as a first-in, first-out (FIFO) fixed-size structure, contrasts with the heap, an unbounded and less organized memory space with an unknown size at compile time. Rust’s memory allocator dynamically locates an available space in the heap, designates it as in use, and returns a pointer representing the address of that location.
Key ownership rules in Rust include that each value can have only one owner, the value is dropped from memory when the owner goes out of scope, and there can be only one owner at any given time.
In Rust, the automatic memory management is facilitated by the drop function, which is called when a variable goes out of scope.
rust
{
// `s` is not valid here; it’s not yet declared
let s = "hello"; // `s` is valid from this point forward
// do stuff with `s`
} // this scope is now over, and `s` is no longer valid
// this scope is now over, and s is no longer valid
Rust uses references, denoted by the & symbol, allowing you to refer to a value without taking ownership of it. References ensure that the data they point to remains valid for the duration of the reference’s lifetime.
let original = String::from("hello");
let reference = &original; //
Rust’s references are immutable by default, meaning they cannot modify the data they point to. However, mutable references, denoted by &mut, allow modifications to the referenced data.
let mut value = 42;
let reference = &mut value; // Mutable reference to `value`
// Modify `value` through the mutable reference
*reference = 10;
A significant restriction on mutable references is that if you have one, you cannot have any other references—mutable or immutable—to the same value.
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ERROR: Cannot have multiple mutable references to `s`
This restriction prevents data races at compile time. Data races occur when multiple pointers access the same data concurrently, at least one of them modifies the data, and there’s no synchronization mechanism.
// In Rust, you cannot mix mutable and immutable references.
let s = String::from("hello");
let r1 = &s; // No problem
let r2 = &s; // No problem
let r3 = &mut s; // BIG PROBLEM: Cannot have a mutable reference alongside immutable references
The Rust programming language enforces the rule that you can either have one mutable reference or any number of immutable references to a value—this is the principle of exclusive mutable or shared immutable references.