of Ben Makuh


Book Reviews


Learning in Public: Rust Structs

Intended audience: web developers like myself who may have some familiarity with lower-level languages and who are interested in hearing more about Rust. I am not writing as an expert, but as a learner.

I recently have begun to learn Rust, a strongly-typed, semi-functional systems language playing on roughly the same terrain as C++. I have been intrigued by it for awhile, mostly because its concept of a "borrow checker" is, as far as I can tell, actually novel. Plenty of new languages come out every year, but for the most part all of them are basically just variations on the same tune. That doesn't seem to be the case with Rust: the language is designed with audacious goals such as having performance on par with C++ but with the ergonomics of a language much higher up the chain of abstraction. The borrow checker seems like a case of actually having your cake and eating it too. You can have both the speed of direct memory management and also memory safety by statically analyzing the code to make sure the code is only modifying memory from one place at a time.

Anyway, you can read more about the language yourself on the internet, but what I thought I'd do here is chart my course a little bit. As I learn more and more about the language, I want to share the things that have tripped me up, confused me, or bent my brain in new ways.

The first thing that has really taken me awhile to wrap my head around is struct vs impl blocks. In object-oriented languages, it is the standard design pattern to lump together data and the functions that work on that data as a single cohesive noun (a class) in the codebase. This is rather handy because it makes for a very discoverable system: autocomplete providers can look at a class and trivially provide a list of methods and properties related to what you're working on whenever you type a period, for example.

Functional languages on the other hand often do not group data and functions in this way, preferring instead to let functions remain independent. This is really quite nifty in other ways, because it allows you to do neat things like build up compositions of utility functions such that over time the new functions you write are generally just amalgamations of what you've already written rather than just reimplementations of essentially the same algorithm but with slightly different semantics this time. OO languages of course encourage large functions to be decomposed, especially when you notice repetition, but in my experience at least that only happens to a limited degree.

So where does Rust sit? Is it like an OO language where data and functionality are grouped together, or is it more like a functional language where those are kept apart? Well, this is what baked my noodle: it's not really quite either of those. Data can be grouped together as structs, and while you can't define a method on a struct, you kinda sorta can by implementing functionality for a given struct. So let's consider an example.

struct State {  age: i32,  favorite_color: String}

In an OO language that struct would be a class and you'd see methods on it like set_age or get_favorite_color or whatever else. In a functional language you wouldn't have quite the same sorts of functions—you wouldn't be mutating the record—but you could realistically have a factory-type function that generates these kinds of records or takes an existing state record and a patch and returns a newly updated record. If you're familiar with Redux in JavaScript Land, that's what's happening in your reducers. You take the existing state and an action and smash them together and return the result. So what does it look like in Rust?

impl State {  pub fn new(age: i32, favorite_color: String) -> Self {    Self {      age,      favorite_color    }  }  fn buddy_the_elf(&self) {    println!("My favorite color is {}!", self.favorite_color);  }}

Now, okay. If you're like me, that looks like a constructor. And maybe the next question on your mind is, "Well, what is the point of separating these out like that? Why not just put the constructor on the struct and call it a class like everybody else does?" The thing I personally thought of was how in JavaScript we used to basically have this situation (remember prototypal "classes" in ES5?) and in 2015 we finally got classes and it felt like we finally could stop hacking our way around the language. So why would a new language knowingly enter into that situation again?

Well, it turns out that it can be kind of handy to separate out data from behavior in this way. You end up with similar discoverability ergonomics to an OO language (If I type let state = State:: I can get an autocomplete list, yay) while also allowing some of the flexibility that keeping things separate provides. For example, other kinds of structs might have favorite colors, and rather than writing the same buddy_the_elf function for each one, we could write this in a generic way using something called "traits" (a feature I'm still learning about, though they seem kinda similar to the concept of an interface) and then we can implement that trait for the struct and voila our generic function can now work on our specific data format.

Are you learning Rust? Did I get something wrong? Feel free to get in touch with me on Twitter and let me know!

© 2023 by Ben Makuh | Subscribe with RSS | Built with Gatsby