A funny thing about programming is: if you don’t feel a bit ashamed of stuff you have written in the past, you didn’t improve. And now that I’m reading some things on this blog again, I feel a bit ashamed. I hardly explained what is so good about Common Lisp, and some other things now feel factually wrong. Now my tastes also changed a lot, giving more value to powerful type systems and safety in general than to the apparent flexibility of dynamic languages.
Anyway, enough of introduction. I’m here to talk about one hell of impressive language called Rust.
The system programming languages’ current scenario
Well, before starting talking about Rust itself, a bit of context: Rust is aimed to be a powerful, expressive, and, more than all, safe system programming language (which I’ll call SPL for shorts from now on). Other SPLs include C, C++, D, and *sighs* Go.
A SPL, of course, is a language designed to create systems, which are softwares that operate the hardware and provide an abstraction for high-level applications. So, you need abstractions closer to hardware, like types that mimic how bytes are moved around, you should be able to control every single piece of performance, so the language should avoid any kind of overhead (like runtime type checking or garbage collection).
C and C++ are the two languages that dominate this scene. Most Operating Systems nowadays are written in C (like Linux), and some in C++ (like Haiku). There are historical reasons for that (for instance, the fact that UNIX is the basis for most operating systems nowadays), but it’s not impossible to replace them.
D was created as some kind of C++ substitute. Unlike C++, it is a proper C superset, but included a Garbage Collector (that may be avoided), true immutable data, a more explicit functional style, among lots of other resources.
Go was created with… modern languages in mind, like Python or Ruby. Which I don’t see how it should work: as I said before, a SPL must not have runtime type checking or garbage collection, and Go features both because they are “more modern” approaches.
Both D and Go try to solve lots of known problems from C and C++ that are now stuck on their design. C is too simple, and it will remain this way. C++ is too bloated, and there’s nothing to be done on it.
Now, Rust aimed at solving a different scope of problems, with a different set of strategies. Rust dares to be safe.
C and C++ aren’t safe. There are lots of ways your program can go wrong: you can have dangling pointers, segfaults, buffer overflows, mutability can hit your threads with a race condition, you can dereference the wrong type…
D and Go solve some of these problems, but not always in a pretty way (like having memory safety using a GC), and not all problems (I’m looking at you, Go’s empty interface).
Now, Rust if safe because it aims to solve most of these problems in compile time.
Every Rust object is immutable by default, unless you specifically mark them as mutable. Besides, there’s no shared memory between threads (which is funny, considering Tanenbaum’s definition of threads). Concurrency is not exactly an unsolved problem, and in fact lots of languages aim to solve it in different ways, like Clojure, Haskell or Scala.
Now, Rust’s type system is very powerful, compared to the cited languages (maybe not C++, but then, C++’s type system became a mess). It’s not as powerful as Haskell’s, but, for a system language, it feels like it.
Enums are actually algebraic data types disguised. It has type inference. It has traits. Variables can’t hold null pointers (you need to use the algebraic type systems to have something similar. The standard library has the Option<T> enum for that). It may even get better with time.
Now, the most impressive thing about Rust’s type system is that it also provides memory safety in compile time. But how is that?
Rust’s type system is linear. It can assure that an object has exactly only one reference, and if that reference is lost, the object will be deallocated. If you want to pass this object somewhere else, you either have to move it (so you won’t be able to use the previous reference again), or to borrow it (you can only borrow objects if the compiler can assure the original reference will keep living after the borrowed one has died).
That makes it possible to predict the lifetime of every object in compile time, and allocate and deallocate them as necessary at runtime, without resorting to a GC (or any other runtime overhead).
Surely you can use raw pointers just like C or C++, when owned pointers and borrowed pointers can’t express some data structure you need, like doubly linked lists. But you need to mark the blocks where you use them as unsafe. So, if you do have a memory problem, you’ll know where to look. There are also data structures for reference counting or garbage collecting.
Rust is still unstable, so you shouldn’t use it for anything that will go for production. It is still a laboratory, where you may explore and try to apply its concepts. But, honestly, among all the modern SPLs, Rust is the one which shows the most potential, by providing an actual new solution for the biggest problem of any SPL without resorting to any runtime tricks: safety.