Rust: First Impressions through the Eyes of a JS/Python coder

📆 Posted on 13 Feb 2022
Tags:  RustDev
Rust: First Impressions through the Eyes of a JS/Python coder
Date
Feb 13, 2022
Tags
Rust
Dev
Over the past few weeks, I discovered Rust and tried it. I started to practice coding in Rust with examples while exploring the capabilities of Rust. Of course, real programming is a completely different matter. To be honest, coding in Rust is a pretty new experience to me. I’m not familiar with “low-level” system languages like C/C++ and Rust, although I do understand some of the concepts. So some impressions might not be accurate as I might have misunderstood something. Please correct me! This post may also contain a lot of my personal biases. So beware!

General Impression

Besides JavaScript and Python, I also had some time working with Go code. My impression when I first heard about Rust is that it is a language that rivals Go (thanks to a few blog posts and YouTube videos). But as I learned more about Rust, it isn’t the case. Rust and Go are made for different purposes.
Coding in Rust feels nice. The keywords in the syntax are usually short, yet still familiar, like mut, pub, fn, impl, and let. One little thing that still bothers me is writing just the variable name without the return keyword when returning something.
As someone who codes in JavaScript and Python, I have this impression that Rust’s syntax is like a weird mix of C/C++, TypeScript, and Python. It also supports a wide range of utility functions like zip() and iter().
fn max_number(a: i32, b: i32) -> i32 { let mut random_string = String::from("hello there!"); if a > b { a } else { b } }
One thing that I noticed is that the compiler is pretty smart and helpful. It can deduct the type of a variable by looking at its value, so you can do something like let hello = "hello" because it’s obvious that it’s an str type. Rust is also more geared for the procedural/imperative style of programming. But like Go, it allows a mix of functional and OOP as well.
Rust is a programming language that’s designed to solve the mess of memory management in C/C++ without having to use garbage collection like Java or Go. It introduced concepts like lifetimes and ownership, which is probably foreign to most programmers.

What I like

Pattern matching

Pattern matching in Rust can be used for control flow, error handling, variable assignment, among other things. One thing about Rust pattern matching is that it’s exhaustive, meaning that you should consider all possible values when using match. Else, Rust will complain and won’t compile at all.
fn is_42(a: i32) -> bool { let result = match a { 42 => true, 420 => false, _ => false, }; result }

The Result type

Rust doesn’t have a try/catch syntax, but it does have the Result type which helps a lot when working with “nondeterministic” code (e.g. reading the contents of a file). The Result type basically will return one of the following values: Ok or Err. We can then do something based on the resulting value. It’s like doing result, err := myfunction() in Go, but cleaner.
For error handling, we can use pattern matching to process a Result type variable like this:
fn main() { let hello = String::from("1"); // returns type Result<i32, ParseIntError> let parsed = hello.parse::<i32>(); let num: i32 = match parsed { Ok(num) => num, Err(_) => { panic!("Cannot parse!"); } }; println!("{}", num); }
Or alternatively, like this:
fn main() { let hello = String::from("1"); // returns type Result<i32, ParseIntError> let parsed = hello.parse::<i32>().expect("Cannot parse!"); println!("{}", parsed); }
In the syntax above you can see parse::<i32>(). This syntax is called turbofish. It’s the same as parse<i32>() in other languages.

The Option type

In JavaScript or Python, we know that we can assign a variable with a value of null or None. In Rust, there’s no null. At first, I was mindblown at this fact. But Rust supports the Option type, which, like the Result type, can be used to return different values. If there’s a value to be returned, it will return a Some(value). Else, it will return None. This is especially helpful if you’re working with a non-void function that might not return a value and wants to enforce the function caller to check for None cases.

.iter(), .collect(), .map(), .zip(), etc

Despite being a systems programming language, Rust has many abstractions and “sugary” syntax which takes care of many small tasks in the language. Want to map an array? You can just use the .map() method instead of using for loops like it was in the 90s (looking at you, Golang). Want to turn something into an iterable? Use .iter(). Want to turn everything in an iterable into a collection? Use .collect(). I know that these features are already a standard in JavaScript or Python. But seeing them in a systems programming language is refreshing.
let one_to_a_hundred = 0..10; let exponents: Vec<i32> = one_to_a_hundred.map(|i| i * i).collect();

Cargo and the whole Rust ecosystem

JavaScript has npm (or even better, yarn) which is pretty decent for a package management system. It also has nvm to manage multiple Node versions. Python on the other hand has a very convoluted package management system such as pip, pipenv, venv, Poetry, conda, etc (see xkcd).
Rust installations on the other hand can be managed easily with rustup. Rust also comes with cargo, which can be used to manage dependencies, run tests, generate docs, and more. Rust also has crates.io, which is basically npmjs.com for Rust. Any library published to crates.io will have its documentation built and published on docs.rs. Rust beats other systems programming languages here.

Other things I noticed

Easy to learn, hard to master

As I said earlier, Rust is a programming language that’s designed to solve the mess of memory management in C/C++ without having to use garbage collection like Java or Go. You might hear that someone working with Rust is sometimes described as fighting the borrow checker, that is the part of the compiler which is responsible for ensuring that references do not outlive the data they refer to. This “fighting the borrow checker” is what makes Rust so hard to master especially if you’re writing a complex project.
Unlike in C/C++ where you can write a memory-unsafe program and compile them (and get segfault at runtime), the Rust compiler is so strict about memory management that it won’t even compile if you write a memory-unsafe program (it’s technically possible with unsafe Rust). I won’t be diving into those concepts in this post, but I might talk about them later in a different post.

The most helpful compiler messages AFAIK

Consider the following program:
fn main() { let name = String::from("Nourman"); name.clear(); println!("Hello {}!", name); }
It will produce a compilation error:
error[E0596]: cannot borrow `name` as mutable, as it is not declared as mutable --> src/main.rs:6:1 | 5 | let name = String::from("Nourman"); | ---- help: consider changing this to be mutable: `mut name` 6 | name.clear(); | ^^^^^^^^^^^^ cannot borrow as mutable For more information about this error, try `rustc --explain E0596`.
What’s happening here?
name is an immutable variable (yes, a variable in Rust is immutable by default). But when we call name.clear(), it’s essentially borrowed as a mutable because you are mutating it. Rust won’t allow that. The compiler even suggests you make name a mutable variable by changing the syntax into let mut name = ...
Consider this other program:
fn main() { let mut name = String::from("Nourman"); let initial = &name[..1]; name.clear(); println!("Hello {}!", initial); }
It will produce an error:
error[E0502]: cannot borrow `name` as mutable because it is also borrowed as immutable --> src/main.rs:4:2 | 3 | let initial = &name[..1]; | ---- immutable borrow occurs here 4 | name.clear(); | ^^^^^^^^^^^^ mutable borrow occurs here 5 | println!("Hello {}!", initial); | ------- immutable borrow later used here For more information about this error, try `rustc --explain E0502`.
Yep, that’s the borrow checker on the action. It detects that an immutable reference is used after a mutable borrow happened. This will risk the reference to point to an invalid memory. So Rust won’t allow you to do that. See? The Rust compiler produces clear and concise messages. It can even sometimes suggests what you should do. That’s helpful!
So that’s my take on Rust as a JavaScript and Python coder. My final verdict is that Rust has a very bright potential to be used as the future’s programming language. Its community is amazing, its ecosystem is already well-thought. I also heard that Linux and Amazon have also switched to Rust in some of their projects. Surely there will be many improvements along the way. It’s a relatively young language, after all.
©
Nourman
Hajar
NOURMAN·COM·NOURMAN·COM·