Going Further With Rust
After writing Getting Started With Rust, the adventure got more interesting, and the resulting code was simplified to look like this
use fastcmp::Compare;
use memmap::MmapOptions;
use std::fs::File;
use std::io::Error;
trait Equal {
fn equal(&self, other: &Self) -> Result<bool,Error>;
}
impl Equal for usize { // Not needed, original template code
fn equal(&self, other: &Self) -> Result<bool,Error> {
Ok(self == other)
}
}
impl Equal for str {
fn equal(&self, other: &Self) -> Result<bool,Error> {
let file1 = File::options().read(true).open(self)?;
let file2 = File::options().read(true).open(other)?;
let size1 = file1.metadata()?.len() as usize;
let size2 = file2.metadata()?.len() as usize;
if size1 == size2 {
let map1 = unsafe { MmapOptions::new().map(&file1)? };
let map2 = unsafe { MmapOptions::new().map(&file2)? };
Ok(map1.feq(&map2))
} else {
Ok(false)
}
}
}
#[cfg(test)]
mod tests {
use std::time::{SystemTime, UNIX_EPOCH};
use super::*;
#[test]
fn it_works() {
let result = <_>::equal(&2, &2).unwrap();
assert_eq!(result, true);
match Equal::equal("Cargo.toml", "Cargo.toml") {
Ok(result) => assert_eq!(result, true),
Err(_) => {eprintln!("Error comparing files")}
};
match Equal::equal("Cargo.toml", "src/lib.rs") {
Ok(result) => assert_ne!(result, true),
Err(_) => {eprintln!("Error comparing files")}
};
println!("Comparing two movies");
let movie1 = "/Users/eric.kolotyluk/Movies/Oppenheimer.2023.1.mkv";
let movie2 = "/Users/eric.kolotyluk/Movies/Oppenheimer.2023.2.mkv";
let start = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
match Equal::equal(movie1, movie2) {
Ok(result) => assert_eq!(result, true),
Err(_) => {eprintln!("Error comparing files")}
};
let end = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
println!("Elapsed time: {:?}", end - start);
}
}
Unlearning
One of the most important practices I am developing with Rust is to stop saying or thinking WTF? I am frequently surprised by what seems unconventional or wrong, but then I have learned: in Rust, it's a different world.
Scala
One of the worst habits I developed in learning Scala was to say WTF? And worse things. ?? I still have a lot of PTSD, and C-PTSD from Scala.
Kotlin
While Kotlin does not make me feel as stupid, it can still be frustrating at times.
Java
Learning Java in 1995, I am impressed at how it continues to keep up, to evolve, and to stay relevant.
Rust
Rust really shines in many ways, but predominately:
New Learning
In the new world of GitHub copilot, AI is not intelligence. While it can be very good at writing new code, and especially good at writing comments, too often it just does stupid useless stuff that makes things worse. I fear a future of software developers depending on such AI, leading us more quickly to the future of "Idiocracy."
Premature Optimization
Because I wanted to my program to be fast, I got caught up in this, and based on prior experience with this same type of program, started using tricks I learned from the past. But the most important lesson I learned was
[package]
name = "rust-files"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
fastcmp = "1.0.1"
memmap = "0.7.0"
let map1 = unsafe { MmapOptions::new().map(&file1)? };
let map2 = unsafe { MmapOptions::new().map(&file2)? };
Ok(map1.feq(&map2)) // Fast Equals
I have not seen anything like this in Java, Scala, nor Kotlin. Maybe it exists, but this was very easy to discover in Rust. In the unit test, this can compare two files at 374 MB/s, on an Apple M3 MacBook Pro.
领英推荐
Originally, I was using fastcmp on Rust Vectors, but by accident, when I started playing with memory mapped files, serendipitously I leaned it works on memory mapped files; it just does the right thing, simply and effectively.
In the olden days, I found Apache's API for comparing files, and while it was simple to use, writing my own comparator in Java with memory mapped files, my solution was four times faster; but I had to play some tricks in Java to get this to work fast.
?
While I knew about the ? operator in Rust, which takes a Result, and uses the Ok value for further computation, it propagates the Err value. While learning Rust, I wanted to develop a better intuition for Rust Result, Ok, and Err, because Rust does not have exceptions like other high level languages.
While Scala, and other languages, pioneered functional error types such as Option, Either, and Try, sadly, exceptions were already baked into the Java runtime.
When trying to write Functional Reactive Programs Scala, one of the biggest problems was inner code could throw exceptions, and often the stack traces were useless.
In Java, the Loom Project has fixed this, where using Virtual Threads can generate a useful stack trace. Implementing Loom was a lot of dedicated and skilled engineering to compensate for one of worst feature of all high level languages: Exceptions.
I am really glad I did not use ? at first, because it forced me to reason better about Rust Results, affirming my experience with Scala Option, Either, and Try, where a Rust Result is like a Scala Try.
Rust also has an Option type like Scala and other languages, because in Rust there are no nulls, and sometimes you don't need to return a Result.
Basically, Rust has omitted two of the worst ever features of modern programming languages, Exceptions and Nulls.
Borrow Checker
People complain about the Rust borrow mechanism all the time, where the Borrow Checker is a feature of the complier to enforce memory hygiene. While I did run into a 'borrow problem' and could not understand what the compiler was complaining about, the root cause was a typo in variable names, where I used the same name twice. Understandably, the Rust compiler is not yet capable of reasoning those kinds of problems.
Idiocracy
Most of my frustration was between GitHub Copilot writing bad code, and the Rust compiler catching it and trying to fix it. Being a Rust newbie, I kept falling into this trap, so the most important lesson of all was learning to recognize this pattern, this dynamic between Copilot, the Rust compiler, and IntelliJ's editor's good intentions. The road to hell is paved with good intentions.
I crave the day when the Rust compiler emits the message:
Have you tried disabling GitHub Copilot?"
But sometimes, even the Rust compiler suggests bad solutions because it cannot magically fix all bad code. The solution is to learn to write better Rust code in the first place.
Tests
Ever since I have been writing tests, they have always been in different files. Writing tests in the same file as the code is new to me, but I like it. Especially when writing a library, I don't have to implement a main() program, or as we called them years ago GLP, or Grungy Little Program.
Inside IntelliJ, it is very easy to run said tests, which means I am more incentivized to write tests, and I could write a whole essay on this, but Rust has really made the right design decision here.
Polymorphism
Rust is not really an Object Oriented programming language such as Java, Scala, Koltin, C#, etc. This is another area of unlearning/relearning, but I am not competent enough with Rust to analyze and comment.
Trying to use polymorphism led me to Rust Traits, where some polymorphism is possible, but let also led to a lot of mistakes and frustration, and while my code does not really need to use Traits, I have left them there for now while I ponder this further.
I have read a lot of articles and watched a lot of video that are critical of Object Oriented programming, and while it feels like sacrilege and heresy, it is best to be agnostic on this, because Rust is not about religion, it is about engineering.