Learning Rust - The Interesting Parts
I have been programming for some time now and have been fortunate enough to have worked on a bunch of different languages like Java, C#, Python, JavaScript (TypeScript).
I have been thinking of picking up a new language for sometime now and the choice came down to Go & Rust. As the title indicates, I went with Rust.
Why Rust?
- The ecosystem: As a frontend/JavaScript developer, I am seeing more and more tools being re-written in Rust for performance benefits, and so hopefully, I would be able to contribute or at least learn from those code bases in the context of my day to day work.
- Its Different?!: All of my previous experiences have been in application-oriented, managed languages. Rust is similar, but has some interesting concepts like Immutability-by-default, Ownership etc.
What is this post?
This post is supposed to be just a journal of things that I might like, dislike or find interesting enough to document as I learn Rust enough to build a simple CLI app. I am following the Rust Programming Language Book and many snippets here are picked from there.
The Setup: Rust and Sublime Text 3 (MacOS)
Packages Required
- Rust Enhanced: The official Rust package for ST3.
- rust-analyzer: Language Server implementation for Rust.
Setup the Language Server
- Install the LSP Package
- Download the right package from releases.
- Extract the package.
- Rename to
rust-analyzer
- Add to
$PATH
- Make it executable
chmod +x rust-analyzer
Enable the Language Server in Sublime
- Open Command Pallete (Cmd + Shift + P)
- Select
LSP: Enable Language Server Globally
- Select
rust-analyzer
The Interesting Parts
Match
Rust has an expressive match
syntax that can be used to compare values. A match expression is written like -
use std::cmp::Ordering;
fn main(){ let guess = 4; // some sample value, can be input let secret = 5; // another value to compare against.
match guess.cmp(&secret) { Ordering::Less => println!("Under"), Ordering::Equal => println!("Match!!"), Ordering::Greater => println!("Over") } }
Each line in the match
block is called an arm
. In my opinion this makes the code very easy to read and understand at a glance.
Built-In Error Handling
Some operations, like I/O always require error handling. Rust’s Result
type represents either a success (OK
) or an error (Err
).
The great thing here is that if a function returns a Result
, then the caller must handle both success and failure cases.
Consider this code that converts a string to an int -
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { eprintln!("{} is not a legit number, try again;", guess.trim()); return () }, };
Immutable Variables
One major difference in Rust vs the other langugages I mentioned above is that variables in Rust are immutable by default.
In cases where we need to re-assign a value to a variable, we need to explicitly mark that variable as mutable by using the mut
keyword.
let a = 5; //This is immutable, a
cannot be re-assigned.
let mut b = 10; // b
can be reassigned.
Shadowing
Rust allows developers to re-declare a variable with the same name in a scope. This is called Shadowing.
Consider the following snippet -
fn main() { let x = 5;
let x = x + 1;
{ let x = x * 2; println!("The value of x in the inner scope is: {}", x); // This will print 12 }
println!("The value of x is: {}", x); // This is still 6. }
Honestly, the first time I saw this, I was pretty confused. Alot of other languages just refuse this kind of re-declaring of variables in scope.
Technically, this variable is still immutable, i.e. this snippet would fail -
fn main() { let x = 5;
x = x + 1; // will fail }
The usecase for shadowing defined in the Rust Book is for running multiple transforms on the same variable without having to declare a bunch of variables names that we don’t care about.
It will interesting to see if this causes any issues on larger Rust codebases.
Arrays
- Arrays in Rust are more like Java than JavaScript. While an array in JavaScript can grow in size after initialization, arrays in Rust are fixed size.
- An array maybe initialized using the type of its elements and the the length like -
let x:[char, 5] = ['a','b','c','d','e']
- Rust also provides a shorthand syntax when we want to create an array with the same elements like
let x = [3;'a']
. This creates an array like['a','a','a']
Expressions vs Statements
Statements do not return a value. An assignemnt is a statement. Consider the following JavaScript snippet;
const a = (b = 12) console.log(a) // prints 12 console.log(b) // prints 12
Here the variable a
gets a value 12, because b=12
returns 12. However, assignments in Rust don’t return a value.
Expressions return a value, Rust is an Expression oriented language.
The block {}
created for new scopes is an expression and thus can be assigned to variable like -
let y = { let x = 3; x + 1 };
Here the last line in the block does not end with a semi-colon (;), thats because expressions do not include ending semicolons, if we add a semicolon, then it becomes a statement and hence it does not return a value.
Return Values
In Rust, the by default, the return value of the function is the value returned by the last expression in the function. Using return
can be used to exit early, but otherwise its optional.
Conditional Assignemnt
Since if
is an expression, conditional assignemnts don’t require additional syntax.
let condition = true; let number = if condition { 5 } else { 6 };
Infinite Loops? There is a keyword for that
Rust has built-in support for infinite loops using the loop
keyword. This is the first language where I am seeing this.
Returning with break;
Another first for me, was the ability to return values with the break
keyword.
let mut counter = 0;
let result = loop { counter += 1;
if counter == 10 { break counter * 2; } };
println!("The result is {}", result);
- Note that since
loop
is treated as an expression that yields a value, we can put it on the right side of assignment. - The
break counter * 2
statement not only stops the loop, but assigns the value to the variableresult
Source https://doc.rust-lang.org/book/ch03-05-control-flow.html#returning-values-from-loops
Ownership
Just refer - https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
Rules of ownership
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Reassigning ‘objects’
Consider the following JS Code:
const x = { foo: 1 } const y = x y.foo = 2 // this works and sets x.foo = 2
Here JavaScript assigns the reference to the variable x to y. So when some code alters y.foo
, it’s actually changing x.foo
.
Consider the following rust snippet
let s1 = String::from("hello"); let s2 = s1; println!(s1); // this errors.
In this case, Rust too copies the pointer to where “Hello” is stored, but does not copy the data itself. However, due to how ownership works, and to keep things simple, let s2=s1
actually invalidates s1
and transfers the ownership of Hello
to s2
. After the re-assignment s1
can no longer be used. This is called move
.
As explained here, this is done so that hello
only has one owner (s2) and when s2 goes out of scope, Rust can easily free up the memory.
Note: Fixed length data like integers and floats, that is stored on the stack can be re-assiged without invalidating the old variable. Read More Here
Ownership and Functions
Passing data to functions as arguments also transfers the ownership of that data. Assume a regular say_hello
function like -
fn say_hello(name: String)->String { String::from("Hello!, ") + &name }
fn main() {
let s1 = String::from("foo");
println!("{}", say_hello(s1));
println!("{}". s1); // This would complain about s1
having moved
}
Passing variables to a function and then storing its return value back in a different variable will obviously be tedios and not always desirable. To get around this Rust has the concept of References
fn say_hello(name: &String)->String { String::from("Hello!, ") + &name }
fn main() { let s1 = String::from("foo"); println!("{}", say_hello(&s1)); println!("{}". s1); }
The &
syntax creates a reference to the value of s1
, but it does not own it. Creating references is called borrowing.
Notes
- References, just like variables are mutable by default. In order to change a reference, it needs to be marked with
&mut
. - There can only be one mutable reference at a time.
Slices
Refer: https://doc.rust-lang.org/book/ch04-03-slices.html
Last Updated: 15 December 2021.