I decided to find out what the deal is with the Rust programming language. It's said to be as performant as C/C++ (henceforth just C for short) but also safer. So I took the plunge off the deep end and started rewriting in Rust a small non-spooling printer server of mine from many years ago. This is by no means a tutorial in Rust; there are resources for that and I'm still learning. It will be a collection of factlets I glean that give insights to the design philosophy. I will update this page, correcting mistakes as I become cognizant of new facts, unlike so many public idiotsfigures who just double down. 😉 I may also reorganise sections when OCD strikes.

Rust is not like C

Don't believe the people who tell you Rust is like C. It doesn't look like C. It looks more like ML.

Rust is like C

However Rust is still an imperative language. Many of the constructs map onto a small number of machine instructions, like C. Rust gives you access to low level operations. Word sizes of various lengths are supported and conversion between different sizes is controlled.

Rust is a typed language with type inference

But often an explcit declaration is not required, though allowed, because Rust can infer the type of many objects. Even numeric constants are typed and checked in expressions, unlike C where promotion happens and may or may not provoke a warning from the compiler.

Mutables are discouraged

Part of the safety of Rust comes from discouraging variables which are altered after being set. Instead one elaborates the computation by deriving new values as execution progresses. So a program ought to look more like a sequence of mathematical statements rather than instructions to poke and peek pigeon-holes in RAM. This mindset permeates programming style in Rust.

Does this affect compiler optimisation? It might even improve it since the compiler doesn't have to map intermediate values to memory locations but can store them in registers and other temporary locations as it sees fit.

But in the real world some variables must change, think I/O, so mutables are available.

Functions can return complex objects

It's not well-known but C does support returning complex objects like structs. This isn't used much, as programmers prefer to work with pointers or the safer references. In the beginning only objects that could fit into a register were returned, so this led to overloading system and library calls to return -1 or 0 for failure. In Rust this isn't done. Functions can return a complex object. Typically this is an enum. Which brings us to:

Enums are not enums as you know them in C

They are more like tagged unions some of you may remember from Pascal, Ada and Modula-2 as discriminated unions. variant records, or a similar name. So typically a function returns an enum consisting of a union which contains either the result on success, or details of the error on error. As the return type is checked strictly at assignment and all variants of the enum must be covered, this hinders programmers from ignoring the 6th Commandment of C Programming by Henry Spencer.

Rust is primarily an expression language

Even constructs that are statements in C, like loops, return a value. This neatly solves some problems. An example is where you search in a loop for the index of a match in an array. In C you have to note the index of the match then break out of the loop. So you have to make the scope of the index variable wider than the loop, mutate its value, and also test for no match. In Rust you combine a let with a while or loop, and the break returns the index of the match. Failing to find a match would return the failure variant of the enum. No scoping problem, no mutable needed.

Another example is the replacement for the ternary operator ?: in C. In Rust this is a let combined with an if expression returning a value.

A semicolon matters

In Rust the last expression evaluated before exiting the function is the return value. This is similar to languages like Ruby. You can put a semicolon after the last statement, but the meaning changes subtly, now the function returns the unit value, (), also called nil. This may be but sometimes isn't what you want.

The return statement is still available for exiting the function before the end. This allows Rust to separate two concerns, the return value, and the flow of control.

Rust has traits

This is an idea from languages like Java, where they are called interfaces. Traits are different from methods. A trait is shared behaviour that is supported by many classes. An example in Ruby is the to_s (to string) method that objects inherit from Object. Note to myself: This is a deep topic and needs more elaboration.

Rust has macros

But not macros as you know them in C. Macros in Rust support expansion of the syntax without breaking type checking. Take for example C's printf. Not all compilers check for match between the format specifiers and the arguments to this variadic function so this can be a loophole for errors. In Rust, the equivalent is println! where the ! indicates that this is a macro, not a function.

Put away that Makefile

Rust uses a tool called cargo to manage a project, from setting it up, building it, through to making releases, something people who have used frameworks like Ruby on Rails will be familiar with. It also manages crates which are packages to extend functionality. Typical crates parse the command line, handle regular expressions, interface to the operating system, and so forth. Crates are versioned and fetched on demand. This means that crate functionality can be extended rapidly while allowing older programs to continue to use older versions. In C a committee would have to approve enhancements to the standard libraries, and then lots of contributed libraries spring up in response to needs in between standard releases.

All imported crates must be declared with the desired version in the [dependencies] section of Cargo.toml. Unlike C includes, it's not sufficient to state the import in the source file.

Rust is evolving

When I picked up my project again after 4 years I found that there are new features in the language and new crates that make life simpler. For example, the ? operator and macro eprintln! appeared.

Error handling

Rust requires that every possible value from an expression be handled so that cases cannot slip through the cracks. As mentioned, in general functions return enums. When starting programming in Rust, you quickly end up with lots of nested ifs or matches. Rust has constructs to make this neater.

The unwrap() method returns the value of the function if there is no error. If an error occurs, the program panics, and you can optionally get a backtrace. This is more likely to be used in development.

The expect() method allows you to modify the string that is displayed by the panic.

The if let Err(e) = expression idiom allows you to handle only the error case of the expression. If there is no error then execution continues after the if. This reduces the nesting levels.

The ? operator applied to a returned Enum causes the error to be propagated to the parent function. It's a bit like exceptions in other languages.

Together these features flatten out the code need to handle all the error cases.

Use functions freely

The ? operator allows you to put a block of code inside a function and collect all the possible error returns in one place in the parent.