Is Rust my new favourite language?

31-12-2019

Over the fast few years I have invested great amounts of time in learning and using various programming languages. Starting out (around 1995!) in a 4GL called OMNIS (apparently still around) I quickly learned Java (from the Dummies book actually) and PHP. With Java and PHP (around 2003), I had a set of tools that I could use to build basically anything I needed: from websites to desktop apps to CLI tools. The languages were reasonably easy to use and deploy, and worked on about every platform I could use.

After this though, the world became more complicated. I wanted to program for embedded devices (Arduino, Raspberry Pi). These required (especially in the beginning) their own convoluted toolchains and C/C++. I wanted to program multimedia with DirectX applications and nice GUIs (not Java Swing, come on) on Windows, which required C/C++ again. I wanted web services that stayed in memory and were fast, so PHP was ditched for NodeJS (and later TypeScript). Then the iPhone came along and I was writing Objective-C in yet another environment.

Needless to say the number of ecosystems, languages and incompatibility between them increased. Around 2016 started looking for a single language that I could use for everything. The candidates back then:

  • JavaScript. You could use this in the browser and on basically all platforms, even with GUIs (Electron) albeit in a bloated way. It wasn’t really suitable for embedded development though and you basically need TypeScript to keep sanity in larger projects. Nevertheless the benefits of sharing code between client/server and the availability of basically all libraries you could ask for was quickly apparent.
  • Swift. This could be used on everything Apple and (a year after introduction) on Linux server-side, which was great. It interfaced nicely with C libraries. Yet it failed to deliver on Windows and took me through about five versions of the language which were not backwards compatible, breaking not only my own code (which I could fix) but a lot of useful packages as well (which often were never fixed).

In 2019 I started investing time in Rust. Rust on appearance fits the ‘one language’ dream quite well:

  • It runs on all platforms I use (macOS, Linux, Windows)
  • It can be used for many types of applications (CLI, web service, even WASM)
  • It is sufficiently low-level to run on embedded systems even, but higher-level tasks do not appear to be difficult like they are in other low-level languages.

Learning Rust in 2019

Needless to say the learning curve for Rust is quite high. Being used to high-level languages like JavaScript and Swift, I suddenly had to think about memory management and other low-level decisions that I hadn’t made since C/C++, which felt like a step back. Once however these decisions are internalized and have become an automatism, you start to see the real logic behind the Rust language.

So, is Rust the one great language? Well…

The tooling is great

  • Sane package manager. I have not (yet?) encountered any of the issues I’ve had with npm before (in particular when vendoring my own packages across repositories). Handling and caching the dependency tree seems a bit better implemented.
  • Compile to native on all relevant platforms, and most packages are cross-platform(ish)
  • Easy to compile to a fully statically linked binary (even without GlibC if using MUSL – this is especially great when cross-compiling).
  • Toolchain is easy to update using rustup.
  • Easy cross-compilation (i.e. I can compile to Raspberry Pi from macOS by installing a separate toolchain using rustup and adding a flag to cargo build).
  • IDE support is good due to RLS (again installed using rustup)

The ecosystem is great

  • Crates.io is like NPM for Rust, only seems a bit cleaner and better organized/less polluted (yet?).
  • Easily find documentation on any package on crates.io simply by going to docs.rs/packagename. Haven’t seen anything this easy since php.net/function_name.

The language is good

  • Rust will ask questions other languages will not. At first it seems daunting to think about whether to use e.g. reference counting (atomic or not?), boxes, et cetera, but these questions are often less asked or implicit in other languages.
  • When it compiles, it will only crash due to runtime mistakes (i.e. you can still have ‘file not found’ or ‘divide by zero’ errors, but no errors related to programming mistakes).
  • Fearless concurrency. It honestly makes you feel like JavaScript where you can have ‘semi-concurrency’ using async/await and the IO threads NodeJS uses in the background, and also have the certainty to not run into concurrency issues.
  • Enough ‘sugar’ to make things easy that should be easy (creating and concatenating arrays of bytes, string handling, error handling), but still fine-grained.
  • Backwards language compatibility with a sane ‘edition’ system for breaking changes (this unlike Swift where between Swift 1 and Swift 5 you rewrite your code five times)

Things I don’t like or am not sure about:

  • Lifetimes still feel complicated – you need to know about them because some situations can only be resolved with annotations, but not often enough to really get an intuition for them.
  • Macros make it really easy to create DSLs in the language (great for things like parsers or even compile-time typesafe SQL) but they are hard to use without documentation and hard to debug (can lead to more cryptic error messages).
  • Error handling is fairly easy with the ‘?’ operator and Result<X,Error> types, but still verbose as it often still requires converting one error type to the other (with map_err; which in some cases is reasonable – i.e. in a web server I will have to convert any error returned from a library to some error message I can present to the user).

Libraries are good

  • Easy things (opening a file, reading a line, bit manipulation, getting system time, etc.) are really easy out of the box.
  • Libraries like serde and reqwest make usually more difficult things such as serialization and HTTP-clients as easy as in JavaScript.
  • Language support for typestates makes it easy to create robust libraries that do not end up in invalid states at runtime (i.e. in a library that controls GPIO pins like this, you have an InputPin and can convert it to an OutputPin only by ‘returning’ the InputPin – i.e. you cannot have both at the same time!).

Obviously quality differs between libraries.

Things that could be improved:

  • Consistency. Unlike Java (which was so structured you could often ‘guess’ an APIs name), Rust prefers brevity in a lot of cases. This means the learning curve is steeper (you have to remember a bit more function names) but in the end, you type less (in Java this used to be solved by providing excellent auto-completion in the IDE).
  • Corner cases of the borrow checker. Especially when dealing with closures, I often end up creating (seemingly needless) clones and copies of values used in the closure from outside. I also feel that IDEs could provide more useful ‘quick fixes’ based on borrow checker messages (often simply making a variable or parameter into a reference is sufficient and obviously what was intended).
  • Cross-compilation with external (C-)libraries. Often these compile when targeting another Linux from Linux, but for some reason don’t play nice when cross-compiling from macOS to Linux, or when using MUSL instead of Glibc.
  • A decent GUI, preferably something based on the Elm architecture / that looks like Vue.js. Iced looks promising.
  • Integration with libraries like TensorFlow. For machine learning you basically still have to use Python or C++. You might use Tensorflow.JS for lighter tasks and Tensorflow for Swift seems promising, but is not complete yet.