Introduction
Rust is a compiled, statically typed, multi-paradigm, modern programming language designed for memory safety and concurrency, without needing a garbage-collector.
What does "modern" mean?
Granted, I've used the word modern, precisely to capture attention. The term is subjective, and many of the features that I consider "modern" are actually concepts borrowed from other languages (more on that later).
However, it's the selection of features and the cargo ecosystem, providing support for dependency management, building, testing, etc., that make the language truly modern.
Statically typed
Let's start with the non-controversial. Being statically typed means data types are known at compile time.
Rust is trying to bring the compiler on your side, by catching as many problems as possible, before your program runs. Knowing data types at compile time allows it to do just that. But that doesn't mean the experience needs to be verbose:
let x : u32 = 10; // type explicitly declared as an unsigned 32-bit integer
let y = x + 5; // y's type is also u32, but it's inferred by the compiler
Memory safety
Rust wants to make sure that you write correct programs.That won't dereference dangling (invalid) pointers, access uninitialized memory (more on this later). Similar to C++'s RAII when things pass out of scope, memory gets freed for you:
use std::fs::File; // "import" from module responsible for file-system interaction
{
// grab a resource
let fileHandler = File::open("foo.txt")?;
//... do stuff with out file
} // when fileHandler goes out of scope, memory is freed
What's also nice is that Rust won't allow you to reference names that are out of scope:
fn add(first : u32, second : u32) -> &u32 {
let result = frist + second;
return &result; // error
} // result gets freed here
result
will get deallocated when execution get out of add
's scope, so returning a reference (via & operator) to it is invalid. (this code would also require something called reference lifetimes, that won't be covered here.)
"Modern"
Here's my favorite part. As previously said, many of these features are inspired from other languages. Rust has been influenced by many languages, learning from their mistakes and not repeating them. It borrows features from functional programming, but the following selection makes it shine, providing support for a multi-paradigm development.
Destructuring
You might be familiar with this from JavaScript. Say we have a 3D point, Point3D
:
//a so called "tuple struct"
// with three unsigned integers corresponding to x,y,z 3D axis
struct Point3D(u8,u8,u8);
We can retrieve only the information we are interested in, by destructuring
let three_d = Point3D(4,6,3);
let (x, y, _) = three_d;
println!("The 2D cooridnates are x: {} and y: {}", x, y);
Pattern matching
This is just precious. Let's say you have a Person
struct, described by name
and age
:
struct Person {
name : String,
age : u8
}
With pattern matching you can do things you've always dreamed about with a traditional switch
statement:
let p = Person { name : String::from("Jennifer"), age : 23 };
match (p.name.as_ref(),p.age) { // match operator pattern matches the result, much like a switch statement, only more advanced
("Jennifer",_) => println!("Oh, Jenn it's you, I need help with the cake, dear."),
(name,minor) if minor < 18 => println!("Dear {}, there's some RobbyBubble for you.", name),
(name, major) if major >= 18 => println!("Dear {}, there's some champain for you.", name),
_ => {} // default case
}
If you noticed, here we also used tuple destructuring. At the match statement, where we created a tuple paired with a person's name and age.
Pattern matching is really powerful and has application in the language other than the match
statement, like if let.
Immutability by default & shadowing
Variables in Rust are immutable by default. Their state is guaranteed not to change. A feature coming from functional programming, in
languages like Haskell or Racket.
C#/Java for example, don't have this feature. The problem with having objects mutable by default is that you have to make sure passing references won't mess up their internal state.
Immutability can prevent annoying debugging situations like asking yourself why suddenly that list is empty? Why that bool you could have sworn should be true
is actually false
. 😑
This is usually solved by good encapsulation, but that's not always possible. Especially when working with third party code that's not in your control.
Also, Rust has a neat feature called shadowing, allowing you to do something like:
// mutable variable explicitly marked so with the "mut" keyword
let mut p = Person{name: String:from(""), age : 24};
// read the name
println!("Please enter your name: ");
// pass mutable name to be read from stdin
std::io::stdin().read_to_string(&mut p.name);
let p = p; // "shadow" old p name, and redefine it as immutable
...
From the last statement, we know nothing will messes up the state of p
, because from there on it's immutable. ✔
No null!
The null reference, or lack therefor of is another reason why I call Rust "modern". Tony Hoare called null reference, his
Rust simply doesn't have null
or any similar keyword. It describes the absence of a value by an optional (see optional type). Achieving this via generics. For example:
let can_have_number : Option<u32> = Option::Some(6);
match can_have_number {
Some(x) => println!("We have the number: {}",x),
None => println!("There is no number to print!")
}
We can wrap any data of any type inside an option, obliging the user to handle both cases. When there is data, wrapped inside a Some
, or when there isn't any. To better understand this, we'll talk about generics and enums.
First class enum support
If you've ever used an enum, you might be familiar with the C style enums.
But in Rust, besides serving as some symbol or integer value enums can have associated values with them.
The Option type you've seen earlier is an enum
, that could be generically defined as:
enum Option<T> {
Some(T),
None
}
Option
has two possible values Option::Some(T)
and Option::None
.
Some
describes the presence of a value. None
describes the lack therefor of.
Notice, for None
, we have no value associated, it's sufficient to say: "there's nothing here". For Some
however, we're not just saying "there is, there is something in here". We're also actually storing and providing the data.
Can you see how that is another use case for pattern matching?
Traits
Traits are like interfaces in Java/C#, they allow for polymorphism, by making structs implement functionality and being able to refer to a struct type, as an interface type.
They differ in that traits can be implemented on types you have no control over.
Imagine using a third-party JPEG library for your very own photo viewer application.
The program, already supports PNG's, Bitmaps and other formats, that use a trait named Photo
. If only that JPEG library had also known about it!😑
Well guess what, in Rust you can implement the Photo
trait upon the JPEG structure provided by the library!
You can even implement self written traits upon built-in Rust data types like u8
, String
, etc. ✔
Conclusions
Together with plenty of other features like borrowing, multi threading and lots of zero-cost abstractions, Rust is a free/open-source modern language.
Developed at Mozilla Foundation, it's enabling all kinds of software, from so called system-programming (embedded, kernel programming) to web apps, via Web Assembly.
If you're interested in learning Rust, I recommend the free Rust book.
Top comments (5)
Great article Dan. As a matter of fact, Tabnine AI code completion was written in Rust :) Did you get the chance to ever use Tabnine?
I've didn't heard of it, will take a look. Thank you!
Thanks! Would love to hear how it goes :)
Nice. This is a good article to introduce Rust first. So Can I translate it into Korean? I'll make sure to leave a source.
Sure, I'd be honored! Glad you liked it! 😀