Home
Featured image of post First Impressions of the Rust Programming Language

First Impressions of the Rust Programming Language

Hello there, and welcome back to my blog. It’s only been… *check’s clock*… more than a month since my last (and first) article!? Well I sure took my time.
insert record player stop sound
Oh boy, that reads kinda funnily. Let me revisit that: I wrote that introduction at the start of November. We’re at the end of March now. I wrote my entire Bachelor’s thesis in the mean time. Yep, this article literally took me longer than my thesis. Oh well. Moving on…

In the mean time, I’ve done a bit of programming in Rust as I’ve been planning for several months already, and I thought: why not make a little write-up out of this? I finally feel like I have sufficient experience in multiple programming languages to be able to make meaningful comparisons. I also wanted to take this opportunity to put my own thoughts about the language in order.

A little disclaimer first: I’m far from a pro in Rust. I’ve only read the first 8 chapters of the Rust book and written about three thousand lines of Rust code as part of a project that I’ve been working on from October to December. If you’re interested, you can find the project here, but it’s quite specific and there are only about 10 people on this planet that should have any interest in it. There’s also nothing too crazy going on in this project; I haven’t explored nearly all features of Rust in it, and I haven’t really made use of anything that isn’t part of the Rust prelude (basically Rust’s standard library). The only dependencies are the regex and rustyline crates after all.
In any case, please forgive me if I forgot covering something important, or perhaps represent something incorrectly. I probably just don’t know better (yet).
With that out of the way, let’s get started.

What is Rust?

In case you didn’t know yet: Rust is a rather young programming language, which only had its first stable release 1.0 in 2015. But despite that, it has had a disproportional impact on the world of software development. There’s already a UNIX based OS written in Rust and a game development engine, Mozilla is porting parts of its code to Rust and so does Discord etc. The Rust Foundation was established only in 2021 with support by Google, Amazon and several other big names.
So you may be wondering, what’s special about Rust? I’ll go over that and much through the course of this article.

Rust Tools

The default Rust install comes with several useful tools, which I want to highlight in the following.

The Compiler

The most important tool is, of course, the compiler, and there actually is a lot to say about the Rust compiler.
First of all: it’s pretty damn impressive. As you might know, one of Rust’s main features is its type system, including the explicit specification of mutability whenever a variable is created or passed, the ownership system etc. While ownership in particular takes a bit of getting used to, the positive effects of these concepts can’t be denied. To give you an example: I use VSCode with the Rust Analyzer extension to code in Rust (PSA: don’t use the Rust extension, it’s deprecated and broken). Whenever I hit Ctrl+S, the extension displays an up-to-date list of all errors and warnings across all files in the current project. Once you’ve fixed all errors, the project is guaranteed to compile without problems. Moreover, the executable produced pretty much will not crash, unless you explicitly wrote it to do so, or otherwise performed one of a few chosen ‘unsafe’ actions. The compiler also all but rules out typical memory-related mistakes such as accessing foreign memory through pointemagic.
As a result of all these factors, you can easily code for several hours straight, then have your program compile first try, while maintaining a close to zero chance of unexpectedly crashing during execution. It was almost eye-opening, especially when I had to do some work in C++ in between, where I had to compile and run my code several times per hour to see if it works.

But the Rust compiler doesn’t quite stop there. Sometimes it suggests the correct solution to the problem right away, and you can pass the error code itself to the compiler to get an explanation and example for the error.

Left: an error from my project produced by removing an import. Right: explanation given by the Rust compiler

Additionally, as the compiler discovers all actual errors in your code, compiler warnings take on a different meaning compared to other languages. While in e.g. C++ you will almost always get warnings when compiling projects of a decent size, these warnings are often there to hint at parts of the code that might produce errors, depending on the circumstances. But often times these warnings can be safely ignored. Most of the time, I find warnings to only induce unnecessary paranoia or obscure the kinds of warnings that are actually important. In Rust, the experience is different. In fact, I don’t think I’ve ever gotten any warning besides ‘unused variable’. And I definitely appreciate those, as they’ve repeatedly pointed me to parts of the code where I forgot to implement a feature I had already prepared functions or structs for.

That said, of course not all meadows are green in Rust land.
One thing to note for example, is that the Rust compiler doesn’t do dynamic linking when compiling your program. Again comparing with C/C++, when I compile my program I often get executables that aren’t so much larger than the source code itself. This is because the executables produced by C/C++ compilers employ static linking, meaning the executable produced will only really contain the part you’ve coded yourself, but all the libraries you’ve used in your program are linked dynamically. That means they are assumed to be present on the machine where the executable will be used and can be loaded at run time.
Rust on the other hand does static linking, meaning your executable is practically stand-alone and doesn’t require any libraries to be installed on your system. Thus the executable must contain the entire code your program ever uses, including all the standard library implementations for simple print functions etc., giving you much larger executables.
To make it more tangible, this program:

fn main() {
    println!("Hello world!");
}

produces an executable of 3.5MiB! With compiler optimizations you can get this down to 1.4MiB, and passing the executable to strip gives you 250KiB, but that’s about all you can do without stepping into the dark realm of Rust nightly builds.
Don’t get me wrong, I’m not arguing against static linking here. There’s pros and cons for either, but this is just something to be aware of if you don’t want to go “Why the hell is the executable for my little script so huge??”, closely followed by accusations of sending around viruses because your little command line script is 3MB.
Yes, this happened to me the first time.

Cargo

Cargo is a tool used to manage Rust projects and their dependencies. Calling cargo new my_proj will create a new project, setting you up with the typical directory structure for Rust projects, including an empty Git repo and an appropriately filled out .gitignore file. It will also include the Cargo.toml file which details build targets and lists all dependencies. The code is compiled by calling cargo build from anywhere within the project directory, which also installs any missing dependencies. This reminds me a lot of pip install -r requirements.txt from Python, except Cargo always does this implicitly when building.
Overall, I really like Cargo. I’m generally not a fan of how some programming languages force you to use very specific directory/file name layouts (looking at you Java) as it just feels like a bother whenever you want to start a small and dirty project, but Cargo absolves you of all the setup here. Automatic install of dependencies is also pretty handy.

Rustfmt

Rustfmt is a formatter for Rust code that also ships with the default install. It is called via cargo fmt and will per default format the entire project’s code base according to the Rust style guide.
Having this is pretty handy. I regularly use it to just quickly write a lot of dirty code and then fix the formatting with Rustfmt. I also think having a sort of ‘official’ code style guide that is so easily enforcable is a good approach.
That said, I don’t agree with all of the formatting choices. For example, the function call

template_replace(REGNULL, &[&null.mod_to_json(), &null.target.to_json(), &null.kind.to_json(), &spd.to_json()]);

was formatted into

template_replace(
    REGNULL,
    &[
        &null.mod_to_json(),
        &null.target.to_json(),
        &null.kind.to_json(),
        &spd.to_json(),
    ],
);

by Rustfmt, and I am not sure I like this. On the other hand, while I haven’t experimented with the settings at all yet, Rustfmt is very customizable according to the repo’s Readme. So perhaps I shouldn’t criticize it for the default choices and just change my settings already.

Rustup

Another tool that I actually can’t say too much about. Rustup allows you to install and manage several Rust installations on your system. I guess this is nice to have, though I’ve personally never felt the need to have more than one version of any language installed simultaneously.
That said, it made cross-compiling (for Windows, from Linux) pretty simple: All I had to do was install the toolchain for Windows, then specify the corresponding target as cargo build --target <target> and that’s it.
I’ve never had to cross-compile anything ever before, but my gut feeling tells me it is probably not this easy in C/C++.

Syntax

There’s a lot that could be said here, but there’s no way I’m going into all the little details here. Rust stays close to the syntax of C/C++ in most cases, but diverges quite a bit in other ways. The function headers for example go their own way, with names coming before types and being separated by double-colons and a bunch of other details, e.g.

fn build_adv_skills(has_aa: &mut bool) -> Vec<AdventurerSkill>

On the one hand, I don’t really care because I don’t see any advantages or disadvantages between writing types first or names first. On the other hand, I don’t understand why you would break with this sort of “tradition” from almost all other programming languages. This still regularly confuses me.

But before diving more deeply into this, I want to go over some of Rusts… I’ll call it “syntactical features” first.
One quite notable thing is how Rust took some inspirations from functional programming languages. This particularly caught my eye after I had an in-depth module about Haskell at uni last semester. The most notable part of this is are match expressions. They are the Rust-equivalent of switch/case statements in other programming languages, but also much more than that. On the one hand, match expressions are, as the name implies, expressions, meaning they return a value. But at the same time they can also contain regular code with little restrictions. Since they are expressions, they can be placed wherever expressions can be placed, e.g. as arguments to function calls, as conditions in if-statements etc.
But as they say, with great power comes great responsibility etc. The flexibility of match expressions allows you to do plain evil things with them. And so I thought, why not write such a (very constructed, of course) example for you? So here you go:

fn test_func(my_var: &mut i32, elem: Element) -> bool {
    let local_var = 13
        * (match my_var {
            13 => 169,
            2 => 12 + *my_var,
            555 => {
                *my_var = 25;
                5
            }
            729 => {
                print!("Good morning!");
                if match elem {
                    Element::Water | Element::Light => true,
                    Element::Wind => false,
                    _ => {
                        if my_var > &mut 150 {
                            true
                        } else {
                            false
                        }
                    }
                } {
                    4
                } else {
                    16
                }
            }
            _ => return false,
        });

    if local_var > 100 {
        return true;
    } else {
        return false;
    }
}

So for the fun of it, I’ll just go through this function with you a little. I think that’s also a good way to get a bit of a feel for Rust syntax. I might have went a bit overboard with this, so just skip the following paragraph if you don’t feel like having an in-depth look at this shitty function.
The function test_func is called with two arguments: the first one is a mutable borrow of a 4-byte integer. Just think of mutable borrows as pointers like in other languages for now, I’ll get into more detail later. The second argument is of type Element, which is an enum I’ve defined in my project. The function then returns a boolean. The formatting you see here was produced by cargo fmt by the way.
So, to go from outside to inside, we have a match expression computing some integer value that is put into local_var and if that value is greater than 100, the function returns true, and false otherwise.
Now the value that gets put into local_var is the product of 13 with whatever gets returned by that abomination of a match expression I wrote.
That match expression matches on the first argument to test_func.
The first case is simple: if my_var contains the value 13, the match evaluates to 169, so that local_var is set to 13 * 169 = 2197. Easy enough.
In the second case, if my_var is 2, we evaluate to 12 + my_var, which in this case is really just fancy-speak for 14.
In the third case, if my_var is 555, the match evaluates to 5… but we also set the my_var variable we got as first argument to 25. Just a bit of side effects for fun and that spicy bit of extra confusion, while we’re at it.
In the fourth case, if my_var is 729, we print a greeting and then, depending on the outcome of the condition in the if, we evaluate to 4 if that condition is true, and 16 otherwise. That condition itself is implemented as another match, this time on the second argument, which is an enum. If that enum has the Water or Light variant, the condition evaluates to true, if it has the Fire element it evaluates to false, and if it is another variant, we go into yet ANOTHER if/else block to finally decide the condition. _ is a wildcard that matches “everything else” by the way.
Going back to the outer match expression again, the fifth and last case matches for all other values of my_var and it directly returns false. To repeat once more: yes, this case directly returns from test_func with value false.

Obviously, you shouldn’t do that. But as long as you don’t, the match expression is pretty nice, and I sometimes wish I had it in other languages too.

There’s a bunch of other little details about Rust syntax that I like, balanced out by a couple others that can make it messy or unwieldy.
For example, I like that you never have to specify types if they are clear from the context (which is rarely not the case), but you always can. This can also be a good way of understanding the code if you’re not sure what you’re working with; You can just annotate everything with the types you think it should have, and the compiler will immediately give you an error if you were wrong.
I also like how tuples are well integrated, there’s no weird constructor necessary here.
Rust also doesn’t have an increment/decrement operator. Not exactly a huge loss, but seems like a bit of an odd choice. Then again, you can do Python-style for-in-range loops anyway, so the use cases for these are already greatly diminished.
I also like that you don’t need braces around if-conditions, but still use curly braces for scopes like every language that is not Python.

Features

I’ll go into a bit more detail on some of the more prominent (or prominently missing) features of Rust.

The ownership system

Perhaps the most significant feature of the Rust language is the distinction between borrowing and moving whenever a piece of data is used.
A borrow is what happens whenever you put an ampersand & in front of the data, e.g. when passing it as a function parameter. If this reminds you of the & operator used in many other languages to get the memory address of a piece of data, then that is not a bad intuition. However, borrows in Rust extend that concept and make some key changes to it.

First of all: while a borrow typically denotes a reference to something, it does not hold a memory address. If you have e.g. an array in Rust, then you can’t get the memory address of its first element, dereference the increment of that address to get the second element etc. In other words, as far as I know, there is no pointemagic in Rust like there is in e.g. C/C++. This is part of the reason why we have great memory safety in Rust.
As borrows really only represent a reference to an object, and not a memory address, we can use them exactly like the original object in most cases. If, for example, you want to call the associated function func on object obj, you’ll write obj.func() regardless of whether obj is a borrow or not. You don’t need to e.g. dereference in the case of a borrow.

However, it wouldn’t be called ownership if that were all. If you pass an object that is not a borrow as parameter to a function, then that object is consumed or moved into that function. It means that function takes ownership of that piece of data and you cannot use it again afterwards. In the end, I don’t think there’s much more to say about it. Having to keep this distinction in mind can often be annoying, but that’s just what we have to put up with for the sake of ASCENDED MEMORY SAFETY

Ahem, back on topic. So, while the ownership system is just something we’ll have to deal with, there are some hiccups when it comes to specifics. This can often lead to unnecessary confusion.
First of all, borrows are practically treated as their very own type. If you’re writing a function that is supposed to take a borrow as parameter, then you can only pass it a borrow in actual use. Yet at the same time, borrows can typically be used the same way as the “original” data could. But this then leads to weird problems.

Let me tell you a little bit about how Rust handles strings and their borrows.
The regular string type is String. If you borrow a variable of type string, you get something of type &str.
Hold on a second… why not &String?
I’m not sure about the exact reason, but I guess they’re making the distinction because &str is not just a string borrow, but much more. &str also represents a string slice. Note that I’ll use “string slice”, “string borrow” and “string reference” interchangeably from here on.
See as an example:

let s = String::from("two words");
let slice = &s[0..3];   // slice contains "two" now

The variable slice has type &str and, well… represents a slice of a string. So in the end, you could say string slices are just another form of string references. Not that exciting yet, but it doesn’t end there. If you just write a value in double quotes, that’s also a string slice. Now you may wonder, Why? Why am I only getting a borrow when I just created that string here?

The answer is slightly technical: if you write a string into your source code, then that ends up unchanged in the compiled binary of your Rust program. The variable containing that string borrow is then realized as a slice of the binary. It is literally a reference to that specific part of the binary that contains this string. After I heard that explanation, I thought… yea sure, that makes sense. And after writing a bit more Rust code, I thought… wow, I hate this. It just feels like very unintuitive design. Why am I getting a string borrow when I just created a new string? I understand the reasoning behind it, but I doubt there’s a serious problem in just making those into actual strings per default. And the technical background here is just a bad argument for bad usability. But anyway, this is of course not a deal breaker of any kind. It does get slighly annoying to have to write String::from() around every other string constant in your code though, and the real problem comes next.

To be honest, most of the time you just want to do something with strings and you really couldn’t care less whether it’s a string or a string slice, or whatever. The problem is, it feels like there is zero consistency on what kind of inputs or outputs functions working with strings expect and produce. You might not care whether what you pass is a string or a string slice, but whatever function you’re using certainly does. Almost none of the functions in the Rust prelude are overloaded to work with both types. As most of the time it doesn’t really matter for the inner workings of the function, you’ll find yourself having to constantly switch whatever you’re working with from string to string slice, then back to string etc., arbitrarily and for no good reason. At the same time there are cases in which you strictly need a string or a string slice and the other one doesn’t work. All these things combined mean that you’ll often run into situations where something very simple just doesn’t work. Let me give you another example:

let s = String::from(" two words ").trim();

The trim() function takes a string and cuts away the leading and trailing white space. This is returned as a string slice, in our case s would contain the string "two words" afterwards.

Simple enough, so what’s the problem with that code?

Well, it doesn’t compile. The problem here is that String::from() creates a new string object and as trim() returns a slice, that is a reference to this object. However, as the newly created string object is never saved in a variable, it practically goes out of scope and is discarded immediately. This would mean the reference created via trim() points to nothing, which is not allowed so the compiler nopes out. The fix to this is very simple, just convert the result returned by trim() to a string object by appending a to_string() call:

let s = String::from(" two words ").trim().to_string();

Both the reasoning behind this and the fix are simple enough, but it’s just another one of those cases where the result is simply unintuitive. Additionally, you have to add this to_string() call when you really just wanted to trim the string. This feels like unnecessary bloat, and while this might not seem like a big deal, imagine having to do this hundreds of times in your code and it starts to just be ugly and annoying.

Similar things happen in multiple places in slightly different ways. Imagine a function that expects a vector of string references as input. But maybe the inputs you wanted to pass are all strings, and now you have to borrow them. Borrowing a vector object itself gives you a reference to the vector of course, not a vector of references. The only way to get a vector of string slices from a vector of strings is by iterating over said vector of strings, borrowing each string and storing that reference into a new vector so you can ultimately pass your newly created vector of string slices to the function. This is another problem that is easy enough to solve, but there simply isn’t a pretty solution, and the fact that this is a problem in the first place is kind of baffling.

If you think about this for a second, you should realize that none of what I described here should be a problem inherent to Rust. You can theoretically run into these problems in any language that uses pointers. The problem in Rust arises from how ubiquitous string references are. Half the text processing functions from the Rust prelude return string slices instead of new string objects, or at least that’s what it feels like. Whenever you write a string in double quotes directly into your code, you get a string slice, etc. You’ll find yourself constantly dealing with string references, where in other languages you mostly deal with actual strings and only pass references in very specific places. Perhaps I’m misrepresenting things here, but in the end I just can’t remember facing these kinds of problems in any other language. I specifically and exclusively associate it with Rust.

Polymorphism

This might be the hardest pill to swallow. Rust is simply severely lacking in this department. It goes its own way when it comes to supporting polymorphism and I’m not sure I like it. Rust doesn’t present itself as an object-oriented language in the first place, but if you want to enjoy the benefits of Rust by using it for your projects, you’re just going to eventually miss the kind of polymorphism that you’re used to from other languages.

To put this into perspective for a second, even if Rust did not have any concept of polymorphism at all, that of course wouldn’t make the language useless or anything. You can probably still implement whatever program you want to make in Rust. But functionality such as inheritance is still incredibly helpful to make your code more intuitively understandable and avoid redundancy.

Traits

Now I gotta say I actually have very little experience working with typical object-oriented language features. They are simply not that relevant for most small projects. But even so, I can recognize places in my code that just scream for a bit of inheritance etc. But to stop beating around the bush and get to specifics, my gripes mostly come down to the fact that Rust doesn’t have inheritance. Well, not quite at least. What Rust does have are so called “traits”. These are pretty much the same thing as interfaces in other languages (I apologize if I’m missing some smaller details here).

Traits basically allow you to do two things:

  1. ensure that different classes implement some shared functionality, while also clearly abstracting that shared functionality
  2. allow for a function with a single implementation to accept different classes as long as they implement a common trait

The first one is mostly useful from a design point of view, as it helps in making the code more understandable. But it also enables the second functionality. A simple example would be Rust’s Eq trait (for equality) which is included in std. Only objects that derive the Eq trait can be compared via ==. You can also restrict functions by multiple traits.

Unfortunately, that only really captures about half of the functionality of good ol' inheritance. Here’s what you cannot do with traits:

  1. inherit data fields
  2. define type generic data fields

The first one is just an inconvenience that leads to code redundance. I can have multiple differents structs inherit the same functions from a trait. Why can’t they also inherit the data fields? Now I just have to write them all again.
The second one is much more important. Imagine for example you want to have a vector that can contain both objects of type A and B. If you wanted to e.g. implement this in C++, you’d just define an abstract superclass C, declare the vector as type vector<C>, then have A and B inherit from C and you’re done. This just doesn’t work in Rust.
Okay, this is not quite true, so let me correct that: if you google for ways to do this in Rust, you’ll find instructions that tell you how to do it. This involves including several external crates, an implementation that is quite a bit longer than what I just described for C++, and a solution that I was neither able to understand, nor get to compile. This is not a joke, I actually tried implementing this (not just for the purposes of this blog, but because I wanted to use it in my project) and I eventually gave up after two hours of trying. It just didn’t work. I cannot remember the last time, if ever, that I wanted to program something concrete and I just couldn’t.

I don’t want to criticize Rust for making this impossible. As there’s instructions online, I’ll accept that it’s possible somehow. The blame is probably on me for doing something wrong here. But I do criticize Rust for making something that’s so simple in many other programming languages so horrendously unintuitive and difficult.

Enums

Okay, rant over. Let’s talk a little about enums in Rust, because they’re actually fairly interesting (compared to most other languages) and they can also be used for some low-level polymorphism. Most importantly, enums can wrap data, with different enum variants able to wrap different amounts and types of data. Here’s a shortened enum definition taken from my project:

pub enum DevelopmentSkillType {
    Encouragement,
    Unknown(String),
    Manifestation(Element, DamageType, u32),
    Bravery(u32),
}

There’s multiple types of development skills. Some consist only of the variant itself, such as “Encouragement”. But the “Bravery” variant also contains a number, the “Manifestation” variant contains a number as well as an element and a damage type etc. Now if you think about it a little, you might realize you can implement at least the “generic data fields” feature missing from traits that I described above, by using enums. If we think back to the vector example with our A-B-C types, you can actually realize that in Rust by defining an enum

pub enum C {
    typeA(A),
    typeB(B),
}

and then declaring your vector as type Vec<C>. In other words, you just define two enum variants, where one variant contains an object of type A and the other one contains an object of type B, and your vector then contains these enums. This does exactly what it’s supposed to do.

So, what’s the problem?

Well, before going deeper, let’s just say I appreciate Rust’s enums a lot. They are much more useful than enums in other languages, they give us this additional polymorphic funtionality etc. In fact, I like them so much that the single longest source code file in my aforementioned Rust project is the one defining all sorts of enums and associated functionality across 1200 lines.

With that out of the way: enums are absolutely horrible for implementing larger/more complex polymorphic relations. For one, almost every time you want to access a piece of data contained in an enum, you’ll have to go through a match statement. A match statement on an enum has to list instructions for every single variant of an enum, the only exception being the default case. This is again not exactly a big deal, but still just leads to code bloat.
I have an enum with 41 variants in my code. While one may rightfully label that as a questionable design decision, Rust doesn’t exactly make it easier. The definition of this enum and its four associated functions stretch on for 400 lines in my code. The majority of that is just taken up by loooong match statements.
Besides, having to implement your objects, then addtionally define an enum just for polymorphism doesn’t seem like great design either. The large matches introduced through large enums, if we had actual inheritance, could just be handled somewhat implicitly by calling an interface method shared between these objects which is just implemented differently in each of them.

You can also achieve polymorphism by just putting the data into the enum variant directly, instead of the object wrapping it. You can see that in the enum example that I took from my project. If the object you want to define that way contains only few data fields, that solution may be nicer to shorten the paths you have to take to access that data. E.g. if you put an object containing that data into the enum, you’ll have to go enum -> object -> data fields. If you put the data into the enum variant(s) directly you can just go enum -> data fields. This method does however become very inconvenient if there’s many data fields contained.
The main problem here is that enums just contain the data in a given order, but they are not associated with a “field name” like they are in structs. Consequently, you also cannot access these fields via their names, but only through their order. Let’s say you have an enum variant that contains five u32 values. That could in theory represent absolutely everything. Age? Income? Amount of memory? Number of times you forgot to brush your teeth? Phone number of that one guy whom you helped with his math homework three years ago? If you wanna access these, you just gotta remember in which order you stick them into that enum and in which order you gotta take them out again.

Of course, if you do something like that, you have no one to blame but yourself. That said, Rust doesn’t exactly encourage good design here. I just wanna go through an object to its associated data. Why do I have to go through an enum and then through an object to get to the data, or otherwise deal with unnamed and arbitrarily ordered fields?

Okay, rant over (again). As I mentioned before, I actually like enums, and I like them even more in Rust. Especially if you have only some very simple polymorphism to implement, I think I actually prefer Rust’s enums over inheritance from other languages, though I still find it inconvenient to have to go through a match most of the time.

Implementation of None

There’s one more detail I wanted to talk about in this section, which is how None is treated in Rust. This is also closely connected to polymorphism, because it is actually implemented via an enum. Rust has an enum Option<T> (where T is a generic) consisting of two variants: None and Some(T). I see this used in mainly two ways:

  1. Replacement for the None type. Every function in Rust that could possibly return None typically defines its return type to be an Option enum. This forces whoever uses that function to explicitly treat the None case. You practically need a match here, which typically has one case to extract the value out of the Some variant, and another case for some sort of error handling for the None variant. I gotta say here, I’ve always heard that None and its treatment is a huge problem in many other languages, but I never had much of a problem with it. Still, Rust solves this problem nice and clean, I guess?
  2. Optional parameters. Yes, the Option enum is used for optional parameters in Rust. If you don’t wanna set the parameter, just pass Option::None!

Well, if that doesn’t sound too great to you, welcome to the club. I gotta say I really don’t like this. Any function implementing optional parameters this way will have to go through another match statement (urgh) just to extract the passed parameters or set the default value instead. For every single optional parameter. This will make the actual function much longer, and you won’t be able to find the default values fit nicely into the function header anymore.

As I see it, optional parameters are a great way to make very complex interfaces much more acccessible. Python does an amazing job at this: there are many functions in e.g. matplotlib or numpy that you can call with very few or even no parameters at all, but at the same time come with a myriad of customization options accessible through optional parameters. The beauty of these interfaces is that all the customization options don’t bother you if you don’t need them, but they’re still accessible if you do. It gets even better since you can actually access these parameters via their names, which helps in making these interfaces more intuitive and easy to remember.

Trying to do the same thing the Rust way with Option enums would be an absolute pain. The matplotlib.pyplot.plot() function in Python for example takes up to 46 arguments, out of which 44 are optional. If this were Rust and you just wanted to plot something by simply passing the two non-optional arguments, then you’d still have to pass Option::None 44 times for the other optional arguments. And the implementation of plot() would probably need 44 match statements to differentiate whether None was passed and the default value should be taken.

Perhaps I’m expecting Rust to be like Python here too much, but still… This is just another case of unnecessary bloat. The Option enum is simply not a pretty implementation for optional parameters. In fact, I dislike this so much that I tend to just arbitrarily agree with myself upon some value that I interpet as a None and replace with the default if it was passed. This is not good style, and Rust’s bad design decisions encourage me to pursue this bad style.
It’s totally not my fault, I swear!!!

Stability

Anyone getting started with Rust should be aware that Rust isn’t exactly super stable. Not as in “unstable release” unstable, but rather that things are constantly changing, and you might sometimes run into troubles you don’t have with the older, more established languages. Rust is a young language and there’s a lot going on with it. The developers do not seem afraid to completely change the way some things work and break a dozen interfaces along the way. I absolutely respect that. In my opinion, many projects (not just in the world of software) tend to focus too much on not breaking existing functionality and thus refuse to fix things that are just bad, all in the name of backwards compatibility or similar.

In the case of Rust, that might still mean updating your compiler will make you unable to compile one of your projects that was still working fine the other day

By the way, looking for help for some problem online and finding a Stack Overflow answer from 2019 that gives you the fix, only for you to realize that the answer is already outdated (but it’s only 3 years old!) is the real Rust experience. I don’t think I’ve ever had that problem with any other language. Don’t take that as criticism though. As I said, I appreciate that they have the courage to go through with huge changes if that’s necessary to make Rust better.

There’s a couple other places where you’ll notice that Rust is still young and a bit rough around the edges. Here’s another problem I ran into:

fn f() -> (i32, i32){
    (729, 42)
}

fn main() {
    let (a,b): (i32, i32) = f();
    let c: i32;
    let d: i32;
    (c,d) = f();
}

We define a function f() that returns a 2-tuple of integers. Then we assign that return twice, once to two variables a and b, and once to variables c and d. This practically destructures the returned tuple value, assigning 729 to a or c and 42 to b or d respectively. This does pretty much the exact same thing twice, the only difference being that we declare the variables before assignment in the second case.
This code gave me the following error:

destructuring assignments are unstable
see issue #71126 <https://github.com/rust-lang/rust/issues/71126> 
   for more information

Okay, a bit inconvenient, but whatever. Guess we’ll just have to go the extra mile by using a match (heh) on the returned tuple to extract the first and second component.

But wait…

The error is only thrown by the second use of the f() function! There’s not much else to say here, honestly. It seems very simple, but doesn’t work. The issue linked in the error message shows that they are aware of this problem, but the fix hasn’t arrived on Rust’s stable branch yet. It does exist on the Nightly branch, which is basically the bleeding-edge unstable version of Rust. That hasn’t changed in the 6 months it took me to write this post by the way. Just another case of Rust being a bit rough around the edges.

The Restrictions of Rust

I talked about the ownership system earlier and how it’s something you get used to. It places a few restrictions on you (the programmer) but also comes with great benefits. What I did not mention so far is that you really have to change the way you implement certain things due to it. Some patterns and concepts that you’re used to from other languages simply cannot be implemented the same way in Rust as what you are used to from other languages.

As another example, I tried to implement a singleton in my project. Since the definition of singleton seems to be somewhat controversial, let me specifiy: I just wanted an object that should only exist once throughout the run time of my program. A simple getter method should allow me to get that object whenever I need it, returning the existing instance of it, or creating it if it doesn’t exist yet. This is one of the classics when it comes to software engineering patterns. In order to get a better grip of Rust, I though I’d specifically google how to implement a singleton in Rust. Not because I didn’t think I couldn’t do it on my own, I just wanted to see how a classic pattern like this would be implemented the Rust way.

Oh boy, what a can of worms I opened there. Perhaps it should have been obvious after thinking about it for just a second, but singletons go against the very principles of the ownership system. That system is all about tracking who has what sort of references to what data, etc. Singletons would circumvent that, as one of their key ideas is being accessible from pretty much anywhere by calling some global getter method. If we break this down a little more, one could look at singletons as just a fancy wrapper around a global variable. Rust doesn’t have global variables at all for the exact reasons described before, the only exception being global constants.

The experience I had here was pretty much the exact same one as when I tried to implement a type generic vector. You can find instructions online. There are several different ways to do this, but they again require external crates to work, code that I didn’t understand or code that I couldn’t get to compile. And again, I gave up after about two hours of trying and decided to not use a singleton. These solutions may use the unsafe keyword which pretty much tells the Rust compiler to ignore the fact that you just wrote code that doesn’t survive the borrow check, and instead crash, should multiple parts of your program try to borrow the singleton at the same time.

This isn’t the only case I encountered where comparitively simple patterns you are used to from other languages either just don’t work in Rust at all, or have to be implemented in ways that are pretty much just working against and around the language itself. My takeaway here is that to work with Rust, at least for larger projects, you’ll have to either rethink the way you write code and implement patterns for Rust, or work against Rust and produce code that is questionable in terms of both memory safety and readability.

Conclusion

Okay, so after I went on and on about all the big and small things that bother me in Rust, let me stress again that Rust can be a great programming language and sometimes super satisfying to write code in. This seems to turn into a sort of tradition on this blog, but as a matter of fact I think it is typically both more fun and more interesting to talk about all the negative things you noticed about <something>.
In any case, nothing beats the feeling of coding for a couple hours on end and then realize that your code does almost exactly what it’s supposed to do the first time you actually test it. That said, Rust is still rough around the edges in so many places, but as the language is still young and a lot seems to happen under the hood, I have hope that many of my annoyances will be fixed in the not-too-far future.
That said, we will also have to adapt our thinking to how certain things work differently in Rust, and come to accept that we cannot apply all of the patterns we were used to from other languages here. In the mean time, I’d still like to recommend Rust to everyone reading this. Just try to do a little project in it, in my opinion it’s an interesting experience. I believe I will continue to use Rust so I can come back to this article in a couple years and point out how much of a know-nothing I was back then.

There’s one or two more really important topics I didn’t cover here, which would be performance of Rust and how parallelization works in it. However, I don’t have any experience with that yet (and the article is already long enough, heh). I plan to come back to this some time in the future and make a proper comparison of C++ and Rust in that regard. Look forward do that in… a couple years… I guess?

Whew, and with that we’re finally done. This easily turned into my longest article, both because it took me months to write, and because I actually had a lot more to say than I thought I would. Or at least the word count tells me so, because it doesn’t really feel like it. I hope I didn’t forget anything, didn’t make too much of a fool of myself and maybe even educated and/or entertained some of my readers along the way. In any case, there’s still a lot of articles I’d like to write. With my Bachelor’s thesis out of the way, I hope I’ll be able to write a lot more in the coming months. Stay tuned o7

Licensed under CC BY 4.0