01 January 2023
11 mins read

What I love about Rust

Features which make Rust stand apart

Shashwat
Shashwat TheTrio

Rust has been one of the most talked about programming languages in the last few years. It’s a systems programming language that’s also fast and fun to use. I’ve been holding off learning Rust for a while now given its high learning curve, but I finally gave it a shot. Having primarily programmed in more traditional languages like Python, Java, Javascript, etc I’ve been pleasantly surprised by how much easier it is to write valid code in Rust.

Below, I go over 3 features that I think make Rust so enjoyable to use.

Sum types

Sum types are a very powerful concept in functional programming. They allow you to represent a value that can be one of a number of different types. For example, a value can be a string, an integer or a boolean. In Rust, this is represented by the enum keyword. Let’s look at an example -

enum Number {
  Int(i32),
  Float(f32),
  Complex(f32, f32),
}

Here we’ve defined a Number type that can be an integer, a float or a complex number. And as you can see, each value of the enum can store different types of data. The complex number for example needs two floating point numbers to store its real and imaginary parts, while the float only needs one.

What this means is that the compiler can guarantee that we’re handling all the cases. For example, let’s look at a function that takes a number and returns its square -

fn square(n: Number) -> Number {
  match n {
    Number::Int(n) => Number::Int(n * n),
    Number::Float(n) => Number::Float(n * n),
    // Number::Complex(n, m) => Number::Complex(n * n - m * m, 2 * n * m),
  }
}

Here, I’ve intentionally commented out the case for complex numbers. If I try to compile this code, I get an error -

   Compiling closure v0.1.0 (/mnt/Shashwat/Github/closure)
error[E0004]: non-exhaustive patterns: `Number::Complex(_, _)` not covered
  --> src/main.rs:10:11
   |
10 |     match n {
   |           ^ pattern `Number::Complex(_, _)` not covered
   |
note: `Number` defined here
  --> src/main.rs:4:5
   |
1  | enum Number {
   |      ------
...
4  |     Complex(f32, f32),
   |     ^^^^^^^ not covered
   = note: the matched value is of type `Number`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
   |
12 ~         Number::Float(n) => Number::Float(n * n),
13 ~         Number::Complex(_, _) => todo!(),
   |

For more information about this error, try `rustc --explain E0004`.
error: could not compile `closure` due to previous error

That isn’t really possible in other languages. You might think that you can achieve this with inheritance but that’s not really the case. Say we have the following C# code.

public interface Number {}
public class Int : Number {int x;}
public class Float : Number {float f;}
public class Complex : Number {int x; int y;}

public static Number? square(Number n) {
  if (n is Int I) {
    return new Int { x = (I).x * (I).x };
  } else if (n is Float F) {
    return new Float { f = (F).f * (F).f };
  }
  return null;
}

This code does roughly the same thing as the Rust one above. But unlike Rust, this code does compile - despite us not handling the case for complex numbers. Because inheritance has so few restrictions(you can inherit a class in a different namespace or module for example) the compiler can’t know how many types are there in the inheritance hierarchy.

This means that its essentially impossible for the compiler to guarantee that we’re handling all the cases. And to be fair, inheritance isn’t aimed at solving this problem - the only reason I mention it here is because I imagine most people coming from an OOP background would think that this is what inheritance is for.

(As an aside, it looks like C# is getting support for sum types sometime in the future. C++ also has tagged unions, which are similar in purpose but not in ease of use)

Returning back to Rust, you can see that the entire language is build around sum types. Say something as simple as parsing a string as an integer.

fn main(){
  let s = "123";
  let num: i32 = s.parse::<i32>();
}

Just like before, this code doesn’t compile. Why? The compiler tells us exactly why -

error[E0308]: mismatched types
 --> src/main.rs:3:20
  |
3 |     let num: i32 = s.parse::<i32>();
  |              ---   ^^^^^^^^^^^^^^^^ expected `i32`, found enum `Result`
  |              |
  |              expected due to this
  |
  = note: expected type `i32`
             found enum `Result<i32, ParseIntError>`

Looks like parse returns a Result and not an i32. And if you’re coming from say Python, where just doing int(s) would work, this is a bit surprising. But this is actually a good thing. The compiler is telling us that parse can fail and we need to handle that case. And this is exactly what we do -

let s = "123";
let num: i32 = match s.parse::<i32>() {
    Ok(n) => n,
    Err(_) => panic!("Cannot parse as int!"),
};

This again is a trivial example but just shows how the entire language forces you to think about error handling. Even when you think nothing can go wrong - say reading a file(what can go wrong there?), the language is telling you that you need to handle the case where the file doesn’t exist, or you don’t have permissions to read it. In other languages, you’d just get a crash at runtime.

This is what makes writing safe code in Rust so intuitive and straightforward. The compiler guides you through the entire process. If you forget to check for a certain case, you won’t have to figure that out through some crash log - the compiler will tell you exactly what case you missed - and refuse to compile until you handle it.

Pattern matching

This is something which goes hand in hand with sum types. To make sum types enjoyable to use and not just a series of if-else statements, Rust has a very powerful pattern matching system(as all good things, inspired by functional languages).

Say I have a struct which holds a name, age and a list of hobbies -

struct Person {
  name: &'static str,
  age: u32,
  hobbies: Vec<&'static str>,
}

fn main() {
  let p = Person {
    name: "Shashwat",
    age: 20,
    hobbies: vec!["Sleeping", "Reading"],
  };
  if let Person {
    age: 20,
    name: "Shashwat",
    hobbies,
  } = p
  {
    println!("Hobbies: {:?}", hobbies);
  }
}

This reads quite nicely. We’re simply saying match the Person if their age is 20 and name is “Shashwat”. And if it is, we bind their hobbies and print them.

But where pattern matching really shines is with enums. I’ll reproduce the match for square from above -

match n {
  Number::Int(n) => Number::Int(n * n),
  Number::Float(n) => Number::Float(n * n),
  Number::Complex(n, m) => Number::Complex(n * n - m * m, 2 * n * m),
}

It is incredibly easy to not only check that the type n is a particular enum member, but to also bind the variables inside that member so that they can be easily accessed.

Expressions

This is something which I think is really cool. In Rust, everything is an expression. This means that you can do things like -

let x = if true {
  1
} else {
  2
};

I find this a much better solution than having two language constructs for if-else - in most C style languages, you have to use if for control flow and ?:(the ternary operators) for expressions.

But ternaries can get unreadable really quickly, so having the normal if construct be an expression is a much better solution. But of course this goes beyond just if - you can do this with loop as well -

let mut i = 0;
let x = loop {
    if i == 10 {
        break i;
    }
    i += 1;
};
println!("{}", x);

loop in Rust is equivalent to while(true) in other languages. But here, we’re using break i to break and evaluate the loop to i And yes, if you forget to write this condition(and thus never break the loop), the compiler will complain.

Conclusion

I know I didn’t mention what most people use Rust for - its focus on memory safety and how it achieves it through borrowing and ownership(as opposed to manual memory management or automatic garbage collection).

But while I think those features are certainly important and make Rust an excellent systems level language, its the features like pattern matching or type inference which make the language so cozy.

And I think that’s what makes Rust so special - contributing to its 7 year run as the most loved programming language.

Hope you learned something new today! If you’re interested in Rust, I recommend checking out the Rust book.

Thanks for reading and have a nice year ahead!

Categories

Rust