Rusty thoughts

Francesco Garosi

Some tools are bigger than others

I always considered programming languages as just plain tools. Well, almost always: when I first started coding in BASIC on my ZX Spectrum, back in 1982, BASIC was the programming language, also because in like 64KB of memory (16 of which were ROM), there was no real possibility to run another compiler or interpreter, except maybe for toy languages like Logo. At the time BASIC was both the language and the shell.

When I first managed to put my hands on a PC -- an old 8088 based Atari, with 512KB of RAM -- things changed significantly: you could explore more possibilities, and I started seeing languages and their compilers or interpreters as just ways to instruct the machine to do something. In those days, the Turbo family of compilers from Borland was almost everywhere: they were very practical tools, easy to use, the editor and the compiler (what we would call an IDE nowadays) were tightly connected, and the experience of using those tools was actually pleasant. Not to mention the fact that those compilers were terribly quick: running a Pascal or C program was a matter of seconds. C looked promising in that period: it had almost the same power that I experienced by coding in the Z80 assembly on my Spectrum, but you (usually) didn't have to deal with registers -- which, let's admit it, is a PITA.

The first "serious" piece of code I wrote in C was a library function that was supposed to copy a rectangle area of the text screen (graphical desktop environments were not so popular yet) into another, and ideally this function had to be used in a program written in (compiled) MS BASIC. The reason is that BASIC was the language of choice where I used to work, and doing the same thing directly in BASIC was simply not possible. Needless to say, the first time I compiled the library and called the function from within my BASIC program, it completely crashed the workstation: C is a very powerful language, and after a reboot I managed to learn that with great power comes great responsibility. Luckily I also managed to learn how to deal with memory in a framework like C, which -- by design -- does not force you to treat memory with the respect it deserves.

I explored many other languages, and toolchains, and frameworks -- with the notable exception of Java which, IMHO, had been designed to employ whole hordes of programmers so that they could write and maintain interminable programs made of unreadable code, which has the expressiveness of a combination of Visual Basic and C++, the verbosity of the neoclassic translation of a greek poem, and the efficiency of an old interpreted language coupled with the convenience of ancient compilers. It was no surprise to me that Java, in fact, decreed the death of poor Sun Microsystems, which I used to love some decades ago.

When I bumped into Python for the first time, it left me astonished. At the time (30 years ago) I was developing a piece of software in Visual Basic, which was several thousands of lines of code (plus a C++ based component, for some CPU intensive operations). I remember that I discovered Python while I was trying to find the scripts of some Monty Python sketches. Knowing the reason why the BDFL named his language like this, maybe it was his intention to attract people like me into his trap. Be it or not, he managed to get me. But the real point is that I started playing with the REPL, I found the language (and the whole framework it comes with) pleasant to work with, and like in one night I managed to write, almost without noticing it, a skeletal but working web-based frontend for the application I was working on. In something more than 100 lines of code. I was shocked. Needless to say, Python became immediately my favorite development environment.

In those days PERL was the tool to choose: it was fast, ubiquitous, had an endless library supported by a myriad of developers to do almost everything. Nevertheless I didn't like it: PERL code was difficult to read, even compared to plain C, and the "there is more than one way to skin a cat" philosophy didn't just meet my taste: it was confusing to say the least, seeing that the very same problem was dealt with in different ways, sometimes in the same piece of software, made everything look like a mess to me. Python, on the opposite side, promotes a context where "there should be one -- and preferably only one -- obvious way to do it": this is not a feature of the language in itself (you can skin a cat in many ways in Python too), but sort of a guideline, which is supported by the combination of the batteries included principle (the standard library, in Python, covers almost everything) and the state-of-the-art documentation, which has accompanied Python from its earliest editions.

Thou shalt write readable code

The much-hated whitespace semantic in Python, which received a lot of criticism in the past, turned out to be one of its most notable strengths: in order to write working code in Python, the code must also be readable. No escape. In fact it's a common thing to hear from pythonistas (including me), that it's easy to dig into old code, maybe left alone for years, and feel comfortable anyway, understanding exactly what it had to do and why it was coded that way. The worst danger that you can have to deal with in such cases is the temptation to amend such old code to something more efficient even if the project is stone dead, but time helps in that sense too and, with some experience, you also learn the ability to leave the past to the past, and that code untouched.

There is more: the mandatory readability of Python makes code understandable even when it has been written by others: in fact, unlike C++ and, even worse, Java, reading good code from someone else is a great source of inspiration and improvement.

This is only to say that, even if a language is just a tool and all languages are computationally equivalent,1 there are some languages that force you to write better code.

Here comes the Rust

I wouldn't advice a beginner (and I still call myself a beginner anyway) to start with The Book to approach Rust, it has to be done, but later. Rust has a great documentation, on par with Python, but there are better ways to get acquainted with this tool/framework/whathever for the first time. I would rather suggest A Gentle Introduction to Rust, which introduces the language from a non-rustacean point of view -- and nobody is a rustacean before she or he becomes a rustacean.

Whatever you choose to do, be it dive into The Book or take it easier by approaching the Gentle Introduction, you soon get confronted with what seems to be a picky, pedantic, and bully hell of a compiler: the authors of both tutorials try to explain that it's not yelling at you, that it does its best at trying to explain what is allowed and what's not, and that it does all of this for your own good. But hey, what language doesn't allow you to change the value of variables in the first place, huh?2

I will not try to explain again what are the principles that inspire Rust, its borrow checker, and so on: both tutorials (and many other good ones too) do this job perfectly. I'll come directly to the point instead: Rust, and its picky, pedantic, and bully hell of a compiler make you think in a different way. In order to write a working program in Rust, you have to play by its rules. Of course this happens with all languages: there are syntax errors in every language, constraint errors in every framework, and so on. Rust, on its side, will not even compile if it finds that you're not playing by its rules in any way. You can tell Rust that you will do something wrong at some point (consider the infamous unsafe keyword), but after writing in Rust for a while you soon discover that you actually want to follow the rules. One of the great advantages of following those rules is that, if you know what you're doing, when your program compiles successfully it's also very likely to actually work as expected, at least without segfaults or other nasty surprises.

I ended up wanting to play by these rules so much that, every time I switch to a more permissive tool like Python, I find myself thinking aberrations such as "OMG, how the hell can I explain to Python that I want this thing to be shared between these threads as an immutable reference?", and so on. Of course you can't, without adding complexity and performance loss (it wouldn't be complicated to write, for example, specific classes for this), and so I have to accept the compromise -- and the fact that, at the worst point ever, the garbage collector starts its job.

There are also some nice bonuses that come with the Rust development environment (with its toolchain, to be precise), and among these there are a formatter and a language server that already integrates with most editors and IDEs. The formatter is a really nice tool: it reformats your code following the standard that you find throughout the documentation, with the possibility to add some customizations when one doesn't really like the defaults. Actually, this reformatted code is really readable: even if Rust doesn't force you to write readable code, it helps you to finally reach this. And the language server is simply great, as you get your errors (and explainations thereof) while you write the code. This means that, most of the times if you follow the suggestions that pop up in your editor, your code might work the very first time you actually build an executable. This is a really satisfactory experience, and everyone needs satisfactory experiences -- that's why chocolate and good wine just exist.

However, as said, this is just a tool. Tools are designed to help doing something, but not everything. So Rust, for instance, will never make you a coffee or feed your pet. Not on its own, at least.

What Rust does

As a language, Rust does everything that can be done in any other language. But there are some things that Rust faces in an original way, and does them increedibly well, mostly by forcing you to behave.

  • Because of the principles behind the concepts of borrowing, which are not just valid in single-threaded processes, but even more when dealing with multithreading, Rust is great at letting you handle concurrency, concurrent data access, and inter-thread communication. One gets so used at Rust's synchronization utilities, that it's really easy to miss them when you switch back to another language/framework which, for instance, does the synchronization job implicitly (GIL anyone?). And soon one gets used to multithreading as a standard, whereas other languages are more naturally single-threaded even though they might implement multithreading in some forms.
  • Saves from the pointer hassle. Pointers are not implicit (references are, or borrows as many call them, but references are not pointers), and if you need to use a pointer for some reason, you have to declare it. The rules that govern ownership and mutability are so strict that you always know who can do what with a particular value. Or better: you might not know, but the compiler (and therefore the language server) does know, and also knows how to pedantically remind you about it. Being saved from the pointer hassle does not mean that you're not in control of memory alllocation, though.
  • Makes you feel in control of the program flow, especially when handling errors: if something can fail it has to declare it (by returning a Result for example), and the path back from a failure is by far more explicit and clear than by handling exceptions.
  • Treats memory with respect: one can decide whether or not to allocate memory (entities that allocate memory are well documented), and whatever you initialize in a place gets destroyed when you leave that place, unless you pass it to somewhere else.

Finally a consideration on mutability of values: it might seem hard to get used to the fact that variables (I prefer to mentally think of values) are by default immutable, but in the end I realized how seldom a value is actually changed in a program in the real world: most of the times, values are intermediate results computed at runtime, and only in a few cases their modification is really needed.

What Rust does not

Apart from not preparing coffee and not feeding pets, there are other things that Rust does not. For example:

  • Rust doesn't fix your bugs. It's smart, tracks the memory you use, gives you a good path to unwind errors, and so on. But it will not spot that entry in that array that you had to delete at a certain point and you forgot to, it will simply believe that you didn't need to delete it.
  • Rust has no "batteries included": if you come from Python, for instance, you'd expect to find almost everything in the standard library. Rust hasn't this attitude: the standard library offers a lot of stuff, that covers basic I/O, data structures, and surprisingly multithreading and synchronization. However the community library is becoming enormous, and many non-std crates (think of regex, tokio, and serde for example), are becoming so popular that they are even mentioned in the official documentation.
  • Rust will not explain a lot of its "magic": how comes there are operators and keywords that insist on items that are only defined in libraries?3 You'll have to assume by faith that it works.

And surely other stuff, that Rust probably shares with other similar tools. However, apart from not fixing logical/operational bugs, many of the problems come with a straightforward solution: the "no batteries included" problem, for instance, is de facto non-existent thanks to the already enormous, and still growing, number of crates that cover almost every possible need.

At the end of the day

Most of the times you find a reference to Rust, somewhere else in the same context the words "steep learning curve" glow, exactly in this order. While Rust might not be the easiest language to learn, especially when you're already proficient in some other imperative language, the people behind the toolset have done their best in order to let you learn by trials and mistakes: since compilation of a program can be really slow even on decent machines, they already have come to your help by delivering a language server that spots your errors while you're coding. This helps in an effective way: even the language servers and linters for Python, which are great and getting better all the time, lack the precision and helpfulness of rust-analyzer.

Moreover, as a language, Rust is both terse and expressive: in this sense it looks like it has been inspired by some of the principles behind Python, which uses very few characters to do a lot of things. Rust too, apart from braces, promotes a concise coding style (few characters per line), is very picky at casing (failing to comply with the style guidelines produces warnings), and with the help of the included formatter it also produces beautifully indented code: that type of code that, when you open it a couple of years after your last edit, you're likely to understand well -- in terms of both what it does, and why you coded it that way. And you might even be tempted to rework it in a more rustacean way, even if the project is stone dead.

One word about the community: these people are really helpful, and it looks like they love to help.


1

apart from the well known Turing equivalence, see the Böhm–Jacopini theorem which explicitly covers the language question.

2

most functional languages, for example.

3

this is blatantly not true: there are a lot of discussions that describe traits such as Future (for the async and .await keywords), FromResidual (for the ? operator), and so on. The Lang items section of the contributor's guide is also helpful in tracking what items are part of the language itself, and to infer how these items can be supported by reserved words or other syntax sugar.