Confused Technology © 2023 James Leonardo. All Rights Reserved. Generated with Night Cafe
Confused Technology © 2023 James Leonardo. All Rights Reserved. Generated with Night Cafe

I starrted dabbling in the new-ish programming language Rust a few months ago. While Rust reached 1.0 in 2015, eight years old is still a baby among programming languages. Nearly everyone taking on the topic “why Rust” will cover the same key features: memory/thread safety, performance, and the ease of distributing applications built with it. Those are the “sell it to the CTO” features. In this post, I cover some of the other features that are a little less flashy but that I think make Rust a pleasure to work with. Fair warning: this post is tech heavy.

The friendly compiler

Rust’s compiler is one of the most, if not the most, friendly compiler I have ever used. (The compiler is the process that turns human readable source code into machine code). The people building Rust’s compiler go to great lengths to make sure that errors are not just complaints that something is wrong, but also give you hints on how to fix it. The code below has an error because I have a variable named ds but mistyped it as dss later:

145 fn do_something() {
146     let ds = "one";
147     println!("{}", dss);
148 }

The compilers for most languages would just complain that dss isn’t defined. Here’s what an error from Microsoft’s C++ compiler would look like:

Error (active)	E0020	identifier "dss" is undefined	MyProject	C:\src\cpp\MyProject\MyCode.cpp	147	

That’s ok. It tells me what’s wrong, but we’ve all had those moments where we’ve spelt something incorrectly and couldn’t for the life of us tell why. Rust’s compiler is much more helpful and tells you it found a similar variable name:

error[E0425]: cannot find value `dss` in this scope
   --> src\rust\mycode.rs:147:20
    |
147 |     println!("{}", dss);
    |                    ^^^ help: a local variable with a similar name exists: `ds`

While it seems small, those extra seconds you save because you see the solution right away help keep you focused on the task you are working on. This type of detail is pretty common in Rust’s compiler errors and that’s important: with Rust’s memory safety safeguards comes potentially complex compiler errors. The team has done pretty good job of telling you exactly what’s going on and usually giving you a good hint of how to fix it. If nothing else, it shortens the amount of time needed when you need to ask someone else for help.

Tests

Anyone who talks to me about what it takes to be successful in software development is going to hear about automated testing a lot (Rule 14: If it isn’t tested, it’s broken.) A cornerstone of automated testing is the unit test. We can loosely define a unit test as a test that validates an individual unit of code (usually, testing a single set of inputs and outputs for a single function). Rust ships with a test framework built in. No need to add extra components, no need to go research additional tools. Just add a single trait at the top of a function and BAM!, it’s a test.

To help learn Rust, I ported my Expressur demo project to Rust: (Code available on GitHub - https://github.com/jimleonardo/expressur_rust). In that project, you can find this test:

#[test]
fn test_evaluate_expression(){
    let expression = "( 1 + 2 ) * 3";
    let expected = dec!(9.0);
    let context = BTreeMap::new();
    assert_eq!(evaluate_expression(expression, &context).unwrap(), expected);
}

Rust ships with a build system named cargo and that’s what you’ll invoke most of the time instead of invoking the compiler directly. With cargo, you can run all the tests in a project with a single command:

cargo test

That will find anything with the #[test] trait and run it. You can also specify part of a test name and Rust will run all tests that match. For example, this will run anything containing “eval” in the name:

cargo test eval

If that’s all it did, I’d say Rust had the minimum automated testing capability out of the box to be called a modern programming tool. There’s a few more features I would like to see added such as parameterized tests, but those features tend to be nice-to-haves. Also, running a specific test from the cargo command line isn’t super straightforward, but that’s pretty minor as most development environments will be able to do that for you.

But wait, there’s more!

cargo test does not just look for #[test] traits on your code to find code that should be tested. It will also look in your documentation comments and run any example code it finds there. Like many C style languages, Rust uses /// to denote a documentation comment. If, within a documentation comment, there’s a bit of code marked off by three ticks (```) above and below, cargo test will run it by default. Here’s an example from the Expressur project:

 1 /// # Examples
 2 ///
 3 /// ```
 4 /// use expressur::expressur::*;
 5 /// use rust_decimal::Decimal;
 6 /// use rust_decimal_macros::dec;
 7 /// let expression = "( 1 + 2 ) * 3";
 8 /// let expected = dec!(9.0);
 9 /// let context = std::collections::BTreeMap::new();
10 /// assert_eq!(evaluate_expression(expression, &context).unwrap(), expected);
11 /// ```

cargo test sees that lines 4-10 are code and will run it. If it fails, it reports the failure just like it will for any other test. This helps keep your documentation up to date, encourages you to include working examples in your documentation, and helps you catch bugs in your documentation.

Find out more about Rust’s testing framework here: https://doc.rust-lang.org/book/ch11-00-testing.html and the documentation testing here: https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html

Documentation is cool?

I never thought I’d consider documentation a language feature, but I feel Rust is doing something almost special when it comes to docs. First, the quality of the documention on https://www.rust-lang.org/learn is pretty high. There is an online edition of the official Rust programming book (https://doc.rust-lang.org/book/) and the community maintains pretty high standards. That’s not to say it’s perfect, just better and more comprehensive than most I’ve run into especially given the relative newness of Rust.

The other aspect of documentation within Rust that I like is the ability to generate good looking documentation from the source code. It’s not doing anything that you can’t do in other languages. What sets it apart is that they’ve got built in ways to take the comments in source code and turn that into web based documentation. Provide your documentation in source code using markdown, and then run cargo doc to generate pretty documentation. I was able to generate the documentation for my Expressur project in a few seconds and here’s an example of the documentation for the evaluate_expressions function:

Example of Rust Documentation
Example of Rust Documentation

That’s generated straight out of the comments next to the code (view the code comments on GitHub). A developer working on the code sees the same documentation in their IDE that another developer sees when the go to the documentation web page. That’s keeping it DRY(Rule 36:”Don’t repeat yourself” : apply that to the computer too)!

Between testing your documentation and making it easy to generate great looking documentation, Rust helps remove barriers that keep us from creating good documentation. Maybe if the promise of Large Language Models like GPT-x comes true, we will see the actual words in our documentation become great too.

Goodbye Billion Dollar Mistake?

The dreaded null reference error is one of the most common bugs in software. It’s also one of the most expensive. The phrase Billion Dollar Mistake was coined by Tony Hoare, the inventor of the null reference, to describe the cost of the bug. Null reference errors occur when we expect to see a value and instead get nothing (null) and fail to handle it properly.

The Expressur demo code does some basic math. You can ask it to evaluate expressions like “a+b”, give it a list of values like [‘a’, 1], [‘b’, 2] and it will look at the expression and return 3. What if you didn’t give it [‘b’, 2]? If Expressur didn’t handle that possibility, you’d get a null reference error. Most languages don’t really give us elegant ways to do that and if we mess it up, we end up doing the ugly thing we call “throwing an exception”. Throwing an exception basically screams “STOP” at the program and then we have to handle it. If we handle it well, we can resume processing. If we don’t handle it well, the program crashes. Even if handled well, that “STOP” costs a lot of processing power. Expressur needs a better way.

Rust gives us that better way through the Result and Option constructs. Result is intended to represent success or failure whereas Option is intended to represent a value that may or may not be present. Otherwise, they are similar concepts, so I’ll just focus on Result. The evaluate_expression function doesn’t return a decimal, it returns Result<Decimal, String> meaning that it will return some decimal value when successful and a string (for a human readable error message) when it’s not successful. The evaluate_expression method looks like this to a caller:

pub fn evaluate_expression(
    expression: &str,
    context: &BTreeMap<String, Decimal>,
) -> Result<Decimal, String>{
    // the code that does the work.
    // If the expression contains unknown variables or is not a valid arithemetic expression, an error is returned.
}

The caller can then handle the success or failure case and knows they need to handle the success and failure cases. Here’s an example of the caller handling the success and error cases:

// our expression
let expression = "a + b";

// context is a list of variable names with their values.

let mut context: BTreeMap<String, Decimal> =  BTreeMap::new();
context.insert("a".to_string(), dec!(1.));
context.insert("b".to_string(), dec!(2.));
    // We declare the context as a mutable (mut) variable because we're
    // going to change it.
    // One of Rust's safety features is that it won't let you change a
    // variable if you don't declare it as mutable.
    // That prevents some oopses.
    // The .to_string() converts the string literal to a String type.
    // That's a bit of weirdness in Rust that exists for a reason, but
    // that's for another post.
    // dec! is a macro that creates a Decimal value from another value.
    // Expressur uses the Decimal type to represent decimal values because humans
    // expect Base-10 numbers and get weirded out when 0.6/3 doesn't equal 0.2.

let result = evaluate_expression(expression, &context);

match result {
                Ok(value) => do_some_work_with_value(value),
                Err(error) => do_some_work_with_error(error),
}

There’s nothing really new to computer science here. We’ve had the ability to wrap values like this in any programming language that supported data structures. However, we often needed to define our own data structure to do it. Rust makes it a primary feature in the language and that means we can expect to see it done the same way from project to project. Consistency helps avoid errors and reduces the amount of time it takes for someone to learn something new. That’s wins all around.

Enumerations

Result and Option are also examples of enumerations in Rust and the match construct you see above will probably clue any developer reading this that enumerations are different in Rust. Enumerations in most C derived languages are just lists of values. In Rust, enumerations are much more powerful because each value of the enumeration can be a wrapper around other values. Let’s use shapes as an example. Here’s a way we could define shapes as an enumeration in C#:

enum Shape {
    Circle = 1,
    Triangle = 2,
    Square = 3,
    Rectangle = 4
}

That’s nice, but we can only use it to tell us something has some shape. In Rust, we can also use enumerations to tell us something about the shape. For example, we could have a Circle that has a radius and a Square that has a side length. We could have a Triangle that is defined by three points and a Rectangle that is defined by two points. Here’s a way that could look in Rust:

// assume Point is a structure with an x and y value.
enum Shape {
    Circle(center: Point, radius: f64),
    Triangle(point1: Point, point2: Point, point3: Point),
    Square(point1: Point, length:f64),
    Rectangle(point1: Point, point2: Point)
}

In the match we used for the Result example, we not only match the Ok or Err value, we also do something with the data value or the data error depending on which result we received. We can do similar things with our Shape enumeration:

let shape = get_shape_from_somewhere();

match shape {
    Shape::Circle(center, radius) => draw_circle(center, radius),
    Shape::Triangle(point1, point2, point3) => draw_triangle(point1, point2, point3),
    Shape::Square(point1, length) => draw_square(point1, length),
    Shape::Rectangle(point1, point2) => draw_rectangle(point1, point2),
}

Now, I know you’re thinking “but we can do that with object oriented programming and encapsulate all of of it”. Rust is not object oriented, at least in the classic sense (traits give you most of the utility you need from classic OO), this gives you another way to do many of the things we’d traditionally turn to OO for and I find it to be much more expressive and intuitive.

Conclusion

A lot of excitement has been generated around Rust’s memory safety and you can take a look at my last post about Security, Safety, and Programming to understand why that matters. Hopefully, this post has helped you see some of Rust’s other features that lead to Rust consistently being ranked as the “most loved” language in Stack Overflow’s annual developer survey. It is not without it’s flaws: that memory safety is very real, but it needs you to solve some problems in different ways and that can lead to frustration. While its performance is widely touted, the C# version of Expressur runs faster (neither the Rust or C# version has been optimized for performance though).

While it’s a great choice for low level systems like operating systems (it’s an option for Linux kernel developers) and web browsers (Chromium, the core of Chrome and Edge, is starting to adopt it), it’s a less obvious choice for more general development. Front end (user interface) frameworks are still maturing and there’s not a lot of push from the existing big framework builders to support Rust as an alternative. That’s not to say that good framework choices don’t exist, just that you should expect that you will need to build more of your own code to do things that you would expect to be covered by a framework in other languages and probably should expect to fix a bug or two. If you expect to build a lot of custom bits and bobs anyway and the prevalance of existing frameworks is a non-issue, Rust is a solid choice.

If you want to learn Rust and do it in a fun way, I recommend Hands on Rust by Herbert Wolverson. In it, you’ll learn Rust by building an adventure game. I think learning a new language by building an entire system like this is a great way and the fact that you get a simple but fun game out of it is icing on the cake.

This has been my most technical article in quite a while, next time I expect a return to more general topics in software development. Stay tuned and see you in May!