Lately I had a chance to implement the same simple prototype in three languages: Python, Go, and Rust.
Python, being my primary programming language in the last 10 years, took me an hour. The version in Go took me a day. And I’ve spent a week (with pauses) to implement the same functionality in Rust.
Frankly speaking, only part of this huge time difference was because of the peculiarities of the Rust language itself. The rest went on working with AWS S3 and Parquet directly, instead of using Duck DB (as I did in Python and Go), because the Duck DB’s Rust binding doesn’t support Map data type. So the comparison is not really fair.
Nevertheless, after spending some weeks with Rust (and falling in love with it) I would still say that Rust is an untroubled language: meaning it is a language for untroubled software developers.
You see, I’ve spent almost all my carreer (except of one year at Metz) being a troubled developer: I was constantly in stress trying to meet deadlines, trying to develop software cheaper, trying to combine user requirements, interests of the customer, business aspects and cutting edge tech into a one perfect product, trying to achieve something big and historically meaningful until I retire.
An untroubled developer is the opposite of all this. No, it doesn’t mean they don’t care about business perspective, user satisfaction or deadlines. It means, they are just not troubled by these and they take their time to do the stuff right.
When I work on a ticket, I already start thinking about it in the shower and on the way to work, and often code first possible solution hours after the start. This soothes my stress.
When an untroubled developer starts working on a ticket, they first make their table clean, rip open the windows to let cool, fresh air inside, straighten the monitors positions, sharpen their pen, open the Moleskine on an empty page, and – first things first – start brewing a cup of coffee.
They don’t have this fear of wasting time.
Sometimes, it is because they are confident that they have enough time.
For me, only this kind of attitude explains why people would want static typing, and even more and more static checks (as Rust checks more statically than C++). And this is what makes Rust attractive to me, already out of respect for this consequent striving for statically correct programs (no matter how much more time they would cost).
One of the fascinating features of Rust that I want to show today, and I mean it unironically, it the fact that you have to unwrap everything before you can start working on it. No, it is not because Rust authors watch too much unwrapping videos on YouTube (they are out of fashion anyway, because TikTok won over YouTube and its videos are not so long).
Unwrapping is how Rust handles errors.
To make it usable, programmer-friendly and readable, they had to unify struct and enum, which is per se a cool idea worth speaking about.
Let’s start with your usual structs, tuples, and enums how we all know them from other languages:
#![allow(dead_code)]
#![allow(deprecated)]
struct Person {
name: String,
age: u8,
civil_status: CivilStatus,
married_date_city: (chrono::NaiveDate, String), //just for fun as tuple
}
enum CivilStatus {
Single,
Married,
Widowed,
}
fn main() {
let pete = Person {
name: "Pete".to_string(),
age: 34,
civil_status: CivilStatus::Single,
// we have to provide some "special" values, because there is
// no null in Rust.
married_date_city: (
chrono::NaiveDate::from_ymd(0, 1, 1),
"(unmarried)".to_string(),
),
};
println!("{}, we need to talk.", pete.name);
}
We can now make this code more pretty, and also marry Pete, because In Rust we can associate tuples with enum members:
#![allow(dead_code)]
#![allow(deprecated)]
struct Person {
name: String,
age: u8,
civil_status: CivilStatus,
}
enum CivilStatus {
Single,
Married(chrono::NaiveDate, String),
Widowed,
}
fn main() {
let pete = Person {
name: "Pete".to_string(),
age: 34,
civil_status: CivilStatus::Married(
chrono::NaiveDate::from_ymd(2010, 5, 12),
"Paris".to_string(),
),
};
println!("Congratulation, {}!", pete.name);
}
and it goes even more beautiful, because we also can associate structs with enum members:
#![allow(dead_code)]
#![allow(deprecated)]
struct Person {
name: String,
age: u8,
civil_status: CivilStatus,
}
enum CivilStatus {
Single,
Married {
date: chrono::NaiveDate,
city: String,
},
Widowed,
}
fn main() {
let pete = Person {
name: "Pete".to_string(),
age: 34,
civil_status: CivilStatus::Married {
date: chrono::NaiveDate::from_ymd(2010, 5, 12),
city: "Paris".to_string(),
},
};
println!("Congratulation, {}!", pete.name);
}
We can now react differently on Petes civil status using template matching:
match pete.civil_status {
CivilStatus::Single => println!("{}, we must talk!", pete.name),
CivilStatus::Widowed => println!("We are so sorry, dear {}", pete.name),
CivilStatus::Married {
date: petes_date,
city: _,
} => println!("{}, remember, you've married on {}", pete.name, petes_date),
};
Now we have everything ready to see how NULL values and possible errors can be handled in Rust.
For starters, there is no NULL in Rust. We use Option instead, which is either something or nothing:
enum Option<T> { // it is just a generic enum
Some(T), // this a value of type T accociated with the enum member
None
}
So, we can extend Person with a possibility to drive a car like this:
#![allow(dead_code)]
#![allow(deprecated)]
struct Person {
name: String,
age: u8,
civil_status: CivilStatus,
license_plate: Option<String>,
}
enum CivilStatus {
Single,
Married {
date: chrono::NaiveDate,
city: String,
},
Widowed,
}
fn main() {
let pete = Person {
name: "Pete".to_string(),
age: 34,
civil_status: CivilStatus::Married {
date: chrono::NaiveDate::from_ymd(2010, 5, 12),
city: "Paris".to_string(),
},
license_plate: Some("NY202".to_string()),
};
match &pete.license_plate {
Option::None => println!("Hello {}", pete.name),
Option::Some(car) => println!("Suspect drives car with license plate {}", car),
}
// alernatively
if pete.license_plate.is_some() {
println!(
"Suspect drives car with license plate {}",
pete.license_plate.unwrap()
)
}
}
Rust doesn’t take jokes, when it is about handling of NULL cases. You have two choices: either you use template matching to write down – explicitely, painfully clearly, in an untroubled state of mind – what has to be done if there is None value in pete.license_plate. Or you do a lazy, sloppy work in a troubled way and just call unwrap() on an Option. This will save you development time, but Rust doesn’t like it when programmers call unwrap() on its Options. So if Rust encounters a None in run-time, Rust will kill you. Literally: the whole execution process will be terminated, without any way to save or stop it.
We can use the same idea for functions that can return errors, using a Result:
enum Result<T, E> {
Ok(T),
Err(E)
}
For example:
use std::fs::File;
use std::path::Path;
fn main() {
let path = Path::new("hello.txt");
let display = path.display();
// Open the path in read-only mode, returns `io::Result<File>`
let _file = match File::open(&path) {
Err(why) => {
println!("couldn't open {}: {}", display, why);
return;
},
Ok(file) => file,
};
// Alternatively:
let _file2 = File::open(&path).unwrap(); // be ready to be killed if file cannot be opened
}
Legends say that in the dark ages, the untroubled knights of light and computer science only had the unwrap() and the template matching as their weapons to handle errors in first Rust editions.
Every single function call that can fail (which is practically every call), had to be unwrapped or template-matched.
Fascinatenly beautiful, clean, responsible and consequent – with no workaround left, with no land to retreat and with the only possible way: to attack the enemy, singing the fighting songs of the ancestors, and to win. Or to stay forever on the battlefield.
People are weaker in the modern day. In the modern day, people like to hide the frightening chaos of reality in small neat innocent-looking boxes. In the modern day, Rust developers have moved further into the rabbit hole and now we can use the “?” operator.
No, “?” is not a censor mark. The operator is literally called “?”.
More on this and on Traits in the next post. Stay tuned.