Please note that I'm still actively learning the language. I'll do my best to describe my current knowledge, but if you find anything to be incorrect, please contact me and I'll see to put it right.
Why Rust?
There were a few other candidates, such as Go and C. I settled with Rust because it's
- a relatively young system language with many 'future-proof' use cases (such as WASM and Cryptocurrencies)
- fundamentally different from Javascript (but perfectly compatible if necessary).
To name a few key distinctions:
Characteristic | Rust | Javascript |
---|---|---|
Implementation | Compiled language | Interpreted language |
Typing | Strictly static | Dynamic |
Memory | Borrow checker | Garbage Collector |
Many sources on the internet assign Rust a steep learning curve. It'd be a great challenge to learn a system programming language.
The following article focuses on the four above characteristics. There will be descriptions and some code examples for you to follow.
For your best reading experience, you should be proficient with Javascript and the Linux / Windows terminal.
Install Rust
Before starting, you'll need rustup
to install the Rust compiler and Cargo, Rust's package manager. For Linux and macOS, install it with the following script:
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
For Windows, download and run the rustup-init.exe
. You can find it here:
https://forge.rust-lang.org/infra/other-installation-methods.html#other-ways-to-install-rustup
Now check if everything was installed properly:
rustup -V # The Rustup toolchain
rustc -V # The Rust compiler
cargo -V # Rust's package manager
Comparing Rust to Javascript - my first impression
I find Rust 'harder' than Javascript in a few ways. Whereas the latter's compiler makes almost no assumptions about how you write code, Rust comes with some opinionated patterns. And that's good, for the language promises a secure ecosystem to develop with.
I felt like knowing Typescript made my experience a bit easier. If you're not ready for a hard cut, you should start off learning it first. You could start by migrating a Javascript app to Typescript.
A simple +1 - function - in Javascript and Rust
Let's look at some code. Both of the following functions take in a user's input and add a 1
, then print it to the console.
In Javascript (Node.js), type the following:
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout,
});
function plusOne() {
readline.question('Enter a number: ', (answer) => {
console.log(`${answer} plus one is ` + (parseInt(answer) + 1));
readline.close();
});
}
plusOne();
The Rust implementation requires a bit more work:
use std::io;
pub fn plus_one() {
let mut user_input = String::new();
println!("Enter a number: ");
io::stdin().read_line(&mut user_input).expect("Could not read user input");
let user_input_san: &str = user_input.trim();
let user_input_num: usize = user_input_san.parse().unwrap();
let result: usize = user_input_num + 1;
println!("{} plus 1 is {}", user_input_san, result)
}
fn main() {
plus_one();
}
A simple +1 - function - differences in the code
My first thoughts were: This code looks super quirky. Let's take a look at the commons and differences:
- You bring modules into play with the keyword
use
- Variables are declared using the keyword
let
and are immutable by default. The keywordmut
allows you to change their value - The
read_line
function returns aResult
type instead of a value. It can be either the expected value OR anErr
object - Instead of accessing a variable directly, you can access its reference by prefixing it with
&
. This mechanism is essential to Rust's 'borrow checking' system - Typing is mandatory and there's an obligatory
main
function
And a few questions popped into my mind (hints included):
- Why is there an exclamation mark behind
println!()
? Hint - What does
String::new()
mean? Is this a constructor? Hint - What in the world is the difference between
Number
andusize
? Hint
You need to consider more concepts in Rust than in Javascript development. Reading about string types alone made me feel naive at times. And I still feel clumsy writing the code.
Let's wrap up the first impression and get to the filet bits.
Distinction 1: The Rust compiler
Javascript is executed in the browser or a Node.js runtime. The code you write will be compiled (= translated) while it runs. The resulting machine code gives your computer instructions.
This type of compilation classifies Javascript as an interpreted language.
When you ship a Rust program, it'll be a single executable file. This is the compiled machine code to be executed by the operating system. Software developed with a compiled language usually performs better. Since all variables, including their types, are known during compile time, the resulting software is less prone to errors as well.
You can compile a single Rust file withrustc <filename>
. It's recommended to usecargo
for bigger projects.
Distinction 2: Static types
The necessity to type variables felt unfamiliar when I did it for the first time. Let's look at the plusOne
functions again for a second, especially at console.log
, to see a possible implication.
function plusOne() {
readline.question('Enter a number: ', (answer) => {
console.log(`${answer} plus one is ` + (parseInt(answer) + 1));
readline.close();
});
}
Can you guess what happens when we remove the outer braces from (parseInt(answer) + 1)
?
A real classic.
Errors like this are less likely to happen in Rust programs. Let's review plus_one
, remove all types and the .expect()
method from io
:
use std::io;
pub fn plus_one() {
let mut user_input = String::new();
println!("Enter a number: ");
io::stdin().read_line(&mut user_input);
let user_input_san = user_input.trim();
let user_input_num = user_input_san.parse().unwrap();
let result = user_input_num + 1;
println!("{} plus 1 is {}", user_input_san, result)
}
fn main() {
plus_one();
}
Let's try to compile and see what happens.
Rust cannot infer the type of user_input_num
. We must ensure type safety before the compiler can do its job.
Since Rust is a statically typed language, the compiler must know the type of all variables during compile time.
Re-add the type annotation usize
to user_input_num
and compile again. You should see the following warning message:
The program still compiles, but it'll show you a possible bug in your code. You'll want to review this line and handle the possible edge case.
If compilation and typing look way too alien, try and give Typescript a shot before you move ahead.
Let's summarize.
Not only does the compiler handle early error detection, but it also warns you where your code may be unambiguous. In situations where you rely on the fail-safety of your program, this behavior is indispensable.
Distinction 3: Memory
When a program runs, its variables and functions are stored in the computer's RAM. It's a valuable resource and must be kept in check.
Javascript uses garbage collection
A garbage collection takes care of freeing memory after a defined ruleset. It regularly checks whether a variable is still in use or not.
The following code gives a simple example of how Javascript manages memory:
// Two objects are created and stored in memory.
// - One that's called `user`
// - One that's called `address`.
// Address is embedded into user.
let user = {
firstName: "John",
secondName: "Doe",
address: {
street: "123 Main Street",
city: "Anytown",
},
}
// We're creating a second reference here.
// `newUser` points to the same data in memory as `user`
let newUser = user;
// Let's also create a reference to the embeddedd address object.
let myAdress = user.address;
// By setting user to `null`, we remove the initial memory reference.
// It cannot be garbage collected yet because it's still referenced by newUser.
user = null;
// Removing the final reference of `newUser` will mark `user` as garbage.
// There's still a reference to `myAddress`, so it cannot be garbage collected yet.
newUser = null;
// By removing the final reference, `user` can be garbage collected.
myAdress = null;
Rust uses a borrow checker
Garbage collection comes with a few tradeoffs:
- Performance of software using GC is hard to predict
- GC runs beside the actual program, adding more workload, which results in reduced performance
- Software using GC does not scale well
In languages such as C, you must manually allocate and free memory. Unfortunately, such manual procedures are often fertile soil for bugs.
Rust uses a unique approach to solve this issue - introducing ownership and the borrow checker. They break down to three simple rules:
- Each value in Rust has a variable that's called the value's owner
- There can only be one owner at a time
- When the owner goes out of scope, the value will be dropped (and memory is freed)
Let's look at some code to highlight how it works. We'll use the example from before and implement a separate function to prompt for the user input.
use std::io;
pub fn prompt() -> String {
let mut user_input = String::new();
println!("Enter a number: ");
io::stdin().read_line(&mut user_input).expect("Could not read user input");
let prompt_value = user_input.trim().to_string();
return prompt_value;
}
pub fn plus_one(user_input: String) {
let user_input_num: usize = user_input.parse().unwrap();
let result = user_input_num + 1;
println!("{} plus 1 is {}", user_input, result)
}
fn main() {
let input = prompt();
plus_one(input);
println!("The initial value was {}", input);
}
If you try and compile the above code, you'll run into the following error:
Let's see why this happens:
- We create the variable
input
. It becomes the owner of the value returned byprompt
. - By using it as an argument for
plus_one
, ownership is passed into the function's scope plus_one
runs andinput
goes out of scope. It's not available to be printed afterward
We can solve this problem in two ways:
- Print the value before executing
plus_one
. - If we don't want to change the order, we can have
plus_one
borrowinput
Borrowing means: We pass a reference to the variable instead of the variable as a whole. Like this, ownership remains constant and the compiler is happy.
You can pass a variable's references by prefixing it with a &
use std::io;
pub fn prompt() -> String {
let mut user_input = String::new();
println!("Enter a number: ");
io::stdin().read_line(&mut user_input).expect("Could not read user input");
let prompt_value = user_input.trim().to_string();
return prompt_value;
}
pub fn plus_one(user_input: &str) {
let user_input_num: usize = user_input.parse().unwrap();
let result = user_input_num + 1;
println!("{} plus 1 is {}", user_input, result)
}
fn main() {
let input = prompt();
plus_one(&input);
println!("The initial value was {}", input);
}
Moving ahead
Let's recap.
You've read a fair share about Rust, Javascript, commonalities, and differences. Especially how Rust implements stricter rules to keep your code efficient.
These concepts look exotic at first. In the end, It's a tradeoff you make. Follow a few simple guidelines. In return, you receive a program that's blazing fast and memory-efficient.
As web developer, there's more to look forward to. Web Assembly allows you to integrate Rust code into Javascript projects, taking full advantage of Rust's safety and performance features. The project maintains an ambitious roadmap which you can find here.