Rust from a Gopher - Lesson 10

Hello and welcome to the sixth post in my series about learning Rust. In case you want to hit it from the start, here’s a link to the first one! This entire series covers my journey from being a completely land-locked Gopher to becoming (hopefully) a hardened Rustacean, able to skitter the hazardous seabed of any application safely.


So Thanksgiving has been and gone, I’ve enjoyed some time off work and got to spend a lot of quality time with my kids and wife. I also managed to complete Bloodborne and achieve the platinum trophy too (a first for me, the game was really enjoyable).

Besides fun and frivolity I also crept my way through chapter 10 of the Rust Book. It covered a lot of ground and exposed large holes in my knowledge of compilers. In a rushed attempt to stymie the shame, I did the deed I know best and picked up another book… Now this probably isn’t the best book to cover compiler holes exactly, but it seemed well-rounded enough to start with. It’s the RISC-V edition of Computer Organization and Design. Over the break I managed to skitter through about 400 pages or so of it. The main part I really enjoyed so far was a concise history of how x86 architecture evolved over the decades. I’m a sucker for reading about this stuff, and I can’t wait for the next visit to the Computer History Museum with my kids.

One toy project I wanted to do in Rust was to write a RISC-V emulator. Just starting with the simplest instruction set (RV32I) didn’t sound too complex. However, when I looked around on the internet I saw this amazing Rust RISC-V emulator where a gentleman by the name of @superhoge not only wrote the emulator for many RISC-V instruction sets, but also compiled it to web-assembly so you can do neat stuff like run Linux on an emulated RISC-V processor in your browser!!! It just blows me away.

Now I’m thinking of going even simpler, perhaps a 4004 emulator would be challenge enough? Though the software selection would be meek. Well enough of my rambling, it’s time to resume my journey through The Rust Book.


10. Generic Types, Traits, and Lifetimes

This chapter covers a lot of ground, though a lot of it is semantics. The technical implications of Rust’s decisions around what’s covered were largely ethereal to me. Perhaps further down the road I’ll grow deeper opinions about the topics covered here and will write again. For now though I’ll do my best to highlight my reactions.

Don’t Shift the Boat

When I was messing around some examples I stumbled across this error when trying to do a 12.0<<3:

$ rustc --explain E0369 [...]

 let x = 12f32; // error: binary operation '<<' cannot be applied to
 x << 2         //        type 'f32'

Is it sad we can’t left shift a f32? I guess the reason it’s disallowed is that some people would expect it to do a binary left shift, and some people would expect it would just increment the exponent value of the floating point number.

In Go, the behavior is weird. If you write something like this, it will convert your float to an int and do the shift:

fmt.Println(12.0<<3) // Prints 96

However if you write this:

fmt.Println(12.3<<3) // Error: ./prog.go:8:18: constant 12.3 truncated

The code will not compile, showing the compilation error above. Then finally, you get another distinct compiler error if you write something like this:

func printShiftedFloat(f float64) {
	fmt.Println(f<<3) 
} // Error: ./prog.go:13:36: invalid operation: f << 3 (shift of type float64)

So what’s happening here? Honestly I don’t know. Is the Go compiler optimizing constant floats to ints at some stage? Why are there two separate compiler errors for trying to left-shift a float? Find out next time in mundane errors for useless code!

Seriously though, I suspect due to severe lack of demand for a binary left shift on a float, added with the ambiguity of doing almost anything else instead, that no language would support this.

Now on to the contents of chapter 10…

Generics

The chapter begins with a slow lead-in, explaining how functions can reduce code duplication. Generics get introduced, showing us how we can further reduce our code footprint by reducing repeated functions with these so called generics.. The Book then proceeds to obliterate my preconceptualizms of generics with this shaken molotov:

We could, for example, implement methods only on Point<f32> instances rather than on Point<T> instances with any generic type.

Well tie me up and call me Carl, this is just about the sweetest thing I’ve heard since my honeymoon! You can do crazy, pointed, shit on your generics, but to only subsets of your choosing. I love that.

Further on in the chapter, the Mixup example (listed below) makes it hard to believe we’re even using a statically typed language at all.

struct Point<T, U> {
    x: T,
    y: U,
}

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y); // Prints p3.x = 5, p3.y = c
}

The Book tells us to worry not though. The language is indeed static. Performance is not impacted by generics. We can sleep safely, because through the mysterious incantations known as Monomorphization Rust makes these mixups possible. It sounds awesome, really. I loved Animorphs growing up a lot and this basically sounds like Rust just reads a whole book and then compiles generics in your program to have support for any of the morphs in that book. Technically if you wanted to morph into an animal from another book, you would have to recompile your code with knowledge of that book too. I don’t think this is a problem though since things like Java’s dynamic class loading aren’t possible in Rust?

Thinking aloud, what’s the alternative to monomorphization in language design - having some form of runtime dynamic dispatch mechanism for funcs? It was about here where the Book gave me a burning desire to read another book, about compiler design. (The last time I actively tried to learn more about compilers was by doing a Coursera course in ~2013, but I got scared off too easily by lex/bison/yacc - I’m open to good recommendations for priming myself here)

Traits

First of all, my predictions from chapter five were only half correct. Traits are interfaces, but there is no duck typing. In Rust, Traits can simply have a default implementation that will be applied to any type when used as a type of said Trait.

My biggest “Holy shit” moment was realizing that you can implement Traits on any type as long as the Trait is local to your crate. This is absurdly awesome. My first thought was “does that mean you can access private members in said implementation too?”. The answer is a big fat resounding NO, or in rustc speak:

error[E0616]: field `vec` of struct `std::string::String` is private
 --> src/main.rs:3:32
  |
3 |         format!("len {}", self.vec.len())
  |                                ^^^ private field

So maybe I was getting too excited about this. It looks more like syntactic sugar for writing functions over a type as methods of a type, for foreign types. Still neat though.

Trait Bound Syntax

So in truth, I actually recoiled slightly when I first saw the &impl TraitName syntax for typing func parameters as Traits. Needless to say, the shock only intensified when I found that even this is syntactic sugar for a longer form! I don’t get why they couldn’t just make it param: TraitName like Go does? Why can’t the compiler just match the type to either a concrete type or a trait based generic?

Moving on I found that composing traits with + was cool. As for using where clauses to more neatly specify traits per generic, is there any other way to compose many traits, concisely and repeatedly? In Go, you can embed interfaces within interfaces.

Blanket Trait Implementations

let s = 3.to_string();

Yesssss. Blanket trait implementations in the stdlib is a choice move. Having to using strcov.Itoa and handle errors, or use fmt.Sprintf in Go to convert your ints to strings is rough. One can argue to_s would have been nicer than to_string but that’s just nit picking!

Lifetimes

let r;

Before getting into the final part of the chapter, let me just pay homage again to Rust’s type inference. Seemingly every chapter, the Book flips me over one last time, roasting me evenly across the gentle flame of this inference system.

Okay so now for Lifetimes.

The chapter starts off by demonstrating what lifetimes look like with a simple example:

 {
        let r;                // ---------+-- 'a
                              //          |
        {                     //          |
            let x = 5;        // -+-- 'b  |
            r = &x;           //  |       |
        }                     // -+       |
                              //          |
        println!("r: {}", r); //          |
}                             // ---------+

This is nice, but given we’re then introduced to using lifetime annotations for function calls, I would have liked a similar diagram with function calls. In fact my main gripe with this section is that I still have no idea what a real world lifetime annotation would look like. The examples felt slightly contrived, and the compiler takes care of the most common/simple cases I’d expect to come across anyway.

Take this example that the book gives us:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

My understanding is that lifetime annotations of longest enable:

  1. Having an upper bound for the returned reference’s lifetime (being the min of the param lifetimes)
  2. Enforcement of scope bounds during compilation for any calls to longest (as direct result of 1.)

Pardoning my compiler ignorance, but I wonder how exactly the rust compiler tracks scope for all references/borrows. I would hazard it can be derived from the AST, but is there some stored encoding of “lifecycle” per borrow that the compiler tracks in order to make these types of checks fast?

Conclusions

Somehow Lifetimes is the first feature of Rust which left me feeling just a little uneasy. I’m hopeful it will turn out to be something I love though, as I understand it better. Time will only tell that tale.

The chapter was rich with features, but finally I’ve learned enough of Rust that I should be a little dangerous now… The next chapter is on testing - something I feel strongly about, and I’m looking forward to seeing what Rust offers up there. Until then though - stay well and see you next time!

comments powered by Disqus