Rust as a High Level Language
Recently I got into a discussion whether Rust is suitable as a high level language (First warning sign: Discussion, second: unclear meaning of terms). So I want to reproduce my argument here, hopefully to the benefit of the reader.
So, what is a high level language? First, our working definition assumes that we talk about programming languages. Historically, a high level language was a language that offered structure beyond plain assembly (or even machine code). However, with the proliferation of C and Fortran, this particular goalpost shifted considerably.
Today, many see e.g. Python or Haskell as high-level languages and C or Fortran as low-level languages (I should however note that this notion is not universally embraced).
A programming language is low level when its programs require attention to the irrelevant.
– Alan Perlis
Of course, as a systems programming language, Rust allows attention to details that are not functional requirements for most software, but may be vital in some special domains, say embedded or operating systems.
Rust has unions (in addition to enums), lets the user control monomorphization and (somewhat) memory layout, and can generally do anything that C could do (but with less undefined behavior).
Some people mistake this to mean that Rust is a low-level language and can not adapt to high-level problems. The opposite is the case: The same qualities that make Rust excel at building abstractions out of low-level moving parts also come in very handy when building high-level abstractions on top.
I know it’s not a complete overlap, but one of the main ways Rust deals with
low-level problems is unsafe
code: This is for parts that are so low-level as
to be opaque to the compiler. When we look into the actual use of unsafe, we
find that the vast majority of Rust code is safe. This is not a coincidence.
One thing that sets Rust apart is that it enables creating nice abstractions on top of the lower levels. What’s more, I don’t need to write all those abstractions by hand, because cargo lets me easily reuse a lot of great Rust code that is out there already.
This of course requires us to be mindful of the interfaces we create. The best crates have beautiful high-level interfaces that offer best-in-class performance nonetheless. The second best solution is to have two sets of APIs, one high-level but with lower performance and one lower-level for those with a need for speed.
Coming back to the question of what sets high-level programming apart from low-level programming, we can see that it is usually working on top of abstractions. And Rust lets us choose – we have crates that do a lot of stuff below the surface with a nice API, e.g. anyhow, regex, serde or diesel.
(Aside: Yes, part of those crates use macros to some extent. No, that is not a shortcoming of the language.)
There are some things that differ from other high-level languages. One notable
difference is the pervasive use of monadic error handling via Result
(which
looks alien to Pythonistas, whereas Haskellers will feel right at home).
The distinction between borrowed and owned values also sometimes results in
some &
and *
sigils in the code, though I don’t find those too distracting.
Finally, local type inference means that often small functions have more header than body. The upside is that the types make the code much more readable.
In summary, Rust spans both the low and high level, and it is up to us, the programmers to make our abstractions not only fast, but usable. I’ll leave you with a link to some guidelines for good Rust APIs.