π

Exploring Rust

Show Sidebar

After being on my list of languages to explore for years following my contribution to Mixxx in the Google Summer of Code 2020 where I got to know C++ professionals who love Rust, I finally start learning Rust last month by implementing a long-anticipated nostr-based project.

First of all, I wanted to get an introduction to the language, so I read a bit in "The Book" - The Rust Programming Language. In a quest for a more interactive experience I found Rustlings and then the Jetbrains adaptation. I have been an Intellij IDEA user for years, displacing Eclipse which I used back in school, especially once I migrated to Kotlin - Jetbrains also made Kotlin so their IDE is of course ahead while also being the de-facto best Java IDE (even though I would love to use fully open-source software like Eclipse, I sometimes need to be pragmatic). But I soon found those courses too slow and basic for somebody of my experience level - instead I opted to get my hands dirty straight away in a project, producing a usable tool within days. In between I read up on some select chapters of The Book and of course did some research.

Common Concepts

Having experience in PHP, Java, Kotlin, JavaScript, Ruby, Python and C++, Rust proved to be an interesting blend of various familiar concepts. Being an expression-based language with native support of iterator pipelines, I found it having a similar feel to Kotlin and other modern languages. One confusion initially came from semicolons - why are they present if Kotlin could completely dispense of them? The answer is that Rust is even more expression-oriented than Kotlin with implicit last expression returns. Which means that semicolons turn an expression into a statement. This article gives some in-depth explanations: https://lucumr.pocoo.org/2012/10/18/such-a-little-thing/

Over time I have found two annoyances in the way Rust handles this though: In its aim for consistency, Rust has the longest ternary-like operator: \\ Where Java writes bool ? x : y, Kotlin writes if(bool) x else y and Rust uses if bool { x } else { y }. It has no separate operator AND does not allow omitting the braces like Kotlin, probably because that would not get along well with parentheses omittal around the condition. Now if any of those return values is an expression and you want the whole to be a statement, you need to add an extra semicolon after it, which becomes a bit cumbersome in match expressions which actually allow omitting braces:

match bool {
    true => statement,
    false => { expression; }
}	  

I was a bit surprised that Rust has an own loop keyword when while true works just as well in my eyes - but due to the focus on expressions as well as ownership which I will come to later, it often makes sense to use a loop without condition and handle exit conditions inside.

A small choice I also found odd are lone single quotes to introduce loop labels, ended by a colon. Why not two equal symbols? Just a small detail that may mess up generic syntax-highlighting. Same for lifetimes which are demarcated by a single quote.

Ownership and Shared State

Now to the meat of what makes Rust unique: Ownership. Oh, this was and is a journey! It made me question multiple times whether I should really write my project in Rust, because it nudges you to micro-optimization by using references and ownership to make the fewest copies needed, which is hardly relevant with the data I am operating. Kotlin nudges you the other way in that: It has a plethora of well-usable but programmatically inefficient helpers in the standard library that lead you to put pragmatism first and only care about optimisation when it really becomes relevant. This is why I love it for prototyping, next to Ruby on Rails on web which also enables you to develop quickly at the expense of not always understanding the magic going on under the hood.

Soon I was confronted with Lifetime Annotations which proved to be very hit-and-miss in the beginning. What helped a great deal was discovering the anonymous lifetime + '_ oddly not mentioned in The Book. But up until now I basically just annotate lifetimes haphazardly until the code compiles or I give up on passing around references and find adequate owned values instead. I even from time to time ask an AI to fix the annotations for me, which worked surprisingly well.

I was a bit confused about sharing generated constants and Application state - I began with Lazy global statics, moved to keeping track of everything in the main function to rediscovering the essentials of object-orientation through Rust structs. Finally, I have an application majorly composed of structs with methods, with a few loose functions around them. One interesting realization was how to break down a struct to make some of its parts mutable - with RefCell, sub-structs and breaking down of blocks. (link fighting borrowchk)

Composition rather than Inheritace

Coming from the world of Java and Kotlin, traits operate quite a bit differently from the equivalent interface. Both provide a level of abstraction, but there is no multi-level inheritance: A struct can implement a trait, that's it. But there are coupled traits in the standard library:

After some time of looking into the inner workings of them, I understood a very interesting thing: By defining implementations of traits separate from the definition of the implementation target, firstly you can implement traits on foreign targets, but more importantly you can implement a trait on an infinity of objects through generic constraints. So what Rust's stdlib does is provide an implementation of Into for everything that implements From, enablind something that feels like inheritance through composition, providing a lot more flexibility while avoiding the overhead of dynamic dispatch and preventing inheritance complications

Yet one thing that keeps me a bit annoyed is the fact that you cannot return a generic Iterator from a function, if the Iterators returned from different branches are not the exact same implementation - and apparently every iterator transformation function produces an own kind of Iterator, something that I was not used to dealing with in Kotlins handy stream processing. So you either return a collection or a Boxed Iterator. Here I am still a bit unsure which once is more applicable in which case, because collecting sounds like an unnecessary overhead, but if the Boxing moves the Iterator from the stack into the heap that also does not sound terribly efficient, depending on the size of the iterator.

Conclusion

Learning Rust is an interesting journey that taught me some novel concepts. It lays a focus on efficiency which is perilous for a perfectionist like me - multiple times have I optimized code to use references instead of copies even for very trivial objects. The good thing is that unlike in my journey with C+ , Rust provides decent guidance and feedback, allowing you to write safe, fast and precise code. I was able to pick it up decently by myself, while my year of coding C+ a few years ago was a struggle and would probably be one again if I picked it back up.

Comment via email (persistent) or via Disqus (ephemeral) comments below: