My experience with Rust as a programming language, prior to the start of this journey, was mainly a need-basis, incomplete and barebones one. I worked on a project as part of my internship @ Akamai, that required me to use the Cloudflare quiche library to build a client capable of performing TLS 1.3 0-RTT Data requests. While I did build it, a lot of the code I wrote was haphazard, and I didn’t really know why or how I was doing things, nor was I conforming to ‘idiomatic Rust’.

It’s been over a year and a half since then, and now I’m determined to formally learn Rust, the right way. This blog will detail my journey in doing so.

The Rust documentation offers a beautifully curated ‘book’ that acts like a roadmap in understanding the language and what it has to offer. Brown University has prettified it and added a bunch of bells and whistles to make what I call the ‘Brown Book’ - this Brown Book is what I’m going to be using as my map.

I’m going to be using this space mainly to write down thoughts about whatever section I complete at whatever point in time. These thoughts may be rants, comments, opinions, jokes, or just plain ol’ infodumps. I’m primarily writing this to keep myself accountable.

Chapter 1. Getting Started

Installing Rust for me was a two-step process - installing rustup and installing cargo, since I had a linker already (anyone who has gcc or clang will have one.)

The cURL to install rustup:

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

strikes me as odd - why force TLS 1.2, I wonder?

rustfmt seems like a super useful formatting tool - I will be using it extensively.

Printing in rust seems to be most easily done via macro - println! (the bang is part of the macro, not me being excited.)

Chapter 2. Programming a Guessing Game

Why do we have JavaScript style ’let’, with type inference happening despite being a statically typed language? :’) I’m already not a fan of C++’s ‘auto’ :/

There’s a lot of chaining involved in Rust. Again, reminiscent of Javascript.

Right now, the whole concept of mut needing to be explicitly mentioned to make a variable mutable feels odd. Why not stick with using const if it needs to be immutable, and default to mutable otherwise?

1..=100 - what is this range syntax 😢

match seems the equivalent of the C++ switch-case, but it works a little differently.

A lot of library functions seem to return Result enums. This consists of Err and Ok. Error handling (a huge selling point of Rust) looks like it’s based off of this. For example, the parse method returns a Result that I have to handle.

let parsed_var: u32 = match some_string.parse() {
    Ok(parsed_val) => parsed_val,
    Err(error) => {
        // Do some error handling
    }
};

This may be getting ahead of myself, but unwrap and expect do this ‘matching’ compactly, if there is no immediate error handling apart from either doing nothing or printing an error message.

let parsed_var: u32 = some_string.parse().unwrap();

and

let parsed_var: u32 = some_string.parse().expect("Parsing failed oopsies");

Chapter 3. Common Programming Concepts

3.1 Variables and Mutability

Variable Shadowing - what even is this?? This feels like someone realised they maybe should’ve stuck with default mutability but had their ego bruised too much. I doubt I’m ever going to use this particular tidbit. The below piece of code feels straight out of a fever (pythonic) dream.

let spaces = "   ";
let spaces = spaces.len();

…just… why??

The rest of this seems vanilla enough, but why. Just… why.

3.2 Data Types

So truthiness is not a thing - fair enough.

This ’tuple’ data type feels kinda odd, I can’t lie. Just give me my struct, man. But I do like that I can just destructure the tuple.

Why can I declare arrays with two structurally similar yet semantically different syntaxes?

let arr1: [u32; 5] // Means an array of u32's of size 5
let arr2: ["Shakaboom"; 10] // Means an array of size 10 with default values as "Shakaboom"

I get that having a default value means you can just infer the type from it but like.. sure, I guess..

3.3 Functions

This ’expression vs statement’ distinction is really lending itself to how rust is setting itself apart from most other languages I’ve worked with. Being able to just return a value from an ’expression’ is pretty wild - it feels like lambda functions simplified.

3.4 Comments

/* Hello everynyan!
How are you Fine Thank You */

// Oh my god!

3.5 Control Flow

The if-else is vanilla ish, with neat interactions with the concept of expressions.

let maybe: bool = if boolean_condition { true } else { false };

The loops, on the other hand, threw me for a loop (get it? xD)

let mut counter = 0;

let result = loop {
    counter += 1;

    if counter == 10 {
        break counter * 2;
    }
};

What is this!? They say the ‘;’ after break is optional, but why? Shouldn’t it just not be allowed, since the last line of expressions when returning a value musn’t have a ‘;’? ;_;

Chapter 4. Understanding Ownership

According to the Brown Book,

Ownership is Rust’s most unique feature and has deep implications for the rest of the language. It enables Rust to make memory safety guarantees without needing a garbage collector, so it’s important to understand how ownership works. In this chapter, we’ll talk about ownership as well as several related features: borrowing, slices, and how Rust lays data out in memory.

Boy am I in for a ride, amirite?

4.1. What is Ownership?

The low-level model is too concrete to explain how Rust works. Rust does not allow you to interpret memory as an array of bytes, for instance.

I’m definitely in for a ride.

Rust does not allow programs to manually deallocate memory. That policy avoids the kinds of undefined behaviors shown above.

Fair enough.

Rust provides a construct called Box for putting data on the heap. For example, we can wrap the million-element array in Box::new.

Alright who’s in charge of naming these? I just want to talk.

The boxed array has now been bound to both a and b. By our “almost correct” principle, Rust would try to free the box’s heap memory twice on behalf of both variables. That’s undefined behavior too! To avoid this situation, we finally arrive at ownership.

First thoughts - C++ std::unique_ptr and its ‘move’ semantics.

Box deallocation principle (fully correct): If a variable owns a box, when Rust deallocates the variable’s frame, then Rust deallocates the box’s heap memory.

I see. And you can only have one owner for a Box at any given time. Alright, but what about the ‘shared_ptr’ usecase, which I’d argue is more common?

Anyway, so my understanding looks like:

let b;
{
    let mut a = Box::new(100);
    println!("a is {}", a);
    (*a) += 1;
    b = a; // Moving ownership of the Box from b to a
}
println!("b is {}", b); // b now owns the Box, and is 101

is valid. (It is, I ran it.) However, I find it interesting that the below snippet works, considering that s was not declared mutable, but the function add_suffix expects a mutable s. So the function ‘coerces’ its stack variable to be mutable, even though the original parameter may not have been.


fn add_suffix(mut s: String) -> String {
  s.push_str(" world");
  s
}
fn main() {
  let s = String::from("hello");
  let s2 = add_suffix(s);
  println!("{}", s2);
}

Of course, ‘cloning’ a variable allows you to move the clone and retain the original, at the cost of double the memory. Sure! Not very good though.

let to_be_cloned_string : String = String::from("This will be cloned");
let cloned_string = to_be_cloned_string.clone();
println!("{}", cloned_string); // This will print "This will be cloned"
println!("{}", to_be_cloned_string); // This will also print "This will be cloned" because it was cloned

let mutated_clone = mutate_string(cloned_string);
println!("{}", mutated_clone); // This will print "This will be cloned has mutated!"
println!("{}", to_be_cloned_string); // This will still print "This will be cloned" because it was cloned and not moved

In another sense, ownership is a discipline of pointer management. But we haven’t described yet about how to create pointers to anywhere other than the heap. We’ll get there in the next section.

“pointers to anywhere other than the heap” I hope you’re talking about references because if not, what.

4.1. References and borrowing

Yes! An acknowledgement that the above model is a very odd, inconvenient one. And, it turns out I was right - we were talking about references! Do they mean what I assume them to mean? That remains to be seen. (Hey, that rhymed!)

References Are Non-Owning Pointers.

Okay.. I think they mean what I think they mean, which is literally just - a way to access it without the move semantics.

I tried the following:

fn mutate_string_by_reference(mut s: &String) { // I am not returning anything
    s.push_str(" has mutated!");
}

fn main() {
  let my_string = String::from("Shakaboom!");
  mutate_string_by_reference(&mut my_string);
  println!("{}", my_string); // This should print "Shakaboom! has mutated!"
}

This resulted in the following error:

cannot borrow `*string` as mutable, as it is behind a `&` reference

Interesting! So I can move and implicitly ‘mut’ify (as I noted above in 4.1), but I can’t reference and do so.

Indirection in Rust is interesting. You seem to be able to implement a ’trait’ to automatically deref. Hm. So no pointer arithmetic? We’ll see.

You probably won’t see the dereference operator very often when you read Rust code. Rust implicitly inserts dereferences and references in certain cases, such as calling a method with the dot operator.

This is very odd. For example:

let x: Box<i32> = Box::new(-1);
let x_abs1 = i32::abs(*x); // explicit dereference
let x_abs2 = x.abs();      // implicit dereference

These two calls are equivalent. Do boxes define abs? Unlikely.. so it’s just weird syntactic sugar? Moreover, something much odder:

let r: &Box<i32> = &x;
let r_abs1 = i32::abs(**r); // explicit dereference (twice)
let r_abs2 = r.abs();       // implicit dereference (twice)

What in tarnation?!?!?!? Why?? HOW??? WHAT????

let s = String::from("Hello");
let s_len1 = str::len(&s); // explicit reference
let s_len2 = s.len();      // implicit reference

…bro.

MORE COMING SOON!

Thanks for watching! :D