Enums used to be my preferred method for handling form keys and sets of named constant values in TypeScript until I discovered as const. Since that revelation, I've bid farewell to enums and haven't looked back - I simply coudn't find a compelling use case for them. However, delving into Rust's enums completely changed my perspective. They're nothing like TypeScript's enums; despite sharing the same terminology, they offer a unique and powerful experience.
In this blog post, I aim to share my insights into Rust enums and draw comparisons with TypeScript. While I'll cover the basics, I acknowledge there's wealth of functionalities in Rust enums that I haven't explored yet. Rest assured, once I delve into those aspects, I'll craft another blog post to share my findings. Now let's dive in!
Let's start with a simple example in TypeScript.
tsenum Shape {Circle,Square,}function printShape(shape: Shape) {switch (shape) {case Shape.Circle:console.log('Circle');break;case Shape.Square:console.log('Square');break;}}printShape(Shape.Circle); // Circle
It's a pretty straighforward example, we've defined an enum called Shape
with two variants: Circle
and Square
.
The printShape
function, in turn, takes a Shape
parameter and utilizes a switch
statement to print the corresponding name of the shape.
Now let's do the same in Rust.
rustenum Shape {Circle,Square,}fn print_shape(shape: Shape) {match shape {Shape::Circle => println!("Circle"),Shape::Square => println!("Sqaure"),};}fn main() {print_shape(Shape::Circle);}
If you not familiar with match, consider it as Rust's intelligent equivalent of a switch statement for enums. It examines the type and performs distinct actions based on its findings. It's like JavaScript's switch statement, but on steroids.
Now you might be thinking, "what's the big deal bro? They're pretty much the same thing." But hold your horses, let me show you something interesting here.
Let's tweak the enums in both TypeScript and Rust by introducing a new shape—let's call it a Triangle
.
tsenum Shape {Circle,Square,Triangle,}
After we added the Triangle
shape, notice that nothing will happen in the TypeScript version.
ts// nothing will happen here, life goes onfunction printShape(shape: Shape) {switch (shape) {case Shape.Circle:console.log('Circle');break;case Shape.Square:console.log('Square');break;}}// Nothing will get printed, no error, no warning, nothing, nada, nil, zilch.printShape(Shape.Triangle);
But in the Rust version, we will get an error.
rustfn print_shape(shape: Shape) {match shape { // missing match arm: `Triangle` not coveredShape::Circle => println!("Circle"),Shape::Square => println!("Sqaure"),};}
If you have rust-analyzer installed in your editor, you will see the error right away; otherwise, you'll encounter them when you run the code.
The compiler will vehemently point out that the Triangle
shape isn't covered in the match statement, preventing oversight in handling new shapes and highlighting Rust's exhaustive enums.
In contrast, TypeScript doesn't enforce exhaustive checking for enums by default. Omitting a variant in a switch statement won't trigger an error, allowing flexibility but introducting the risk of overlooking cases.
Great, now allow me to share another fascinating aspect I've discovered about Rust enums. To illustrate, let's delve into the following example.
rustenum Shape {Circle(f64), // Circle variant with radiusSquare(f64), // Square variant with side lengthTriangle(f64, f64, f64), // Triangle variant with three side lengths}fn main() {let circle = Shape::Circle(5.0);let square = Shape::Square(4.0);let triangle = Shape::Triangle(3.0, 4.0, 5.0);// you can access associated values like thisif let Shape::Circle(radius) = circle {println!("Circle with radius {}", radius); // Circle with radius 5}}
-
The enum
Shape
now has three variants (Circle, Square, and Triangle). TheCircle
variant has an associated value of type f64 (floating-point number) representing the radius. TheSquare
variant has an associated value of type f64 representing the side length. TheTriangle
variant has three associated values of type f64, representing the three side lengths. -
In the main function, instances of the
Shape
enum are created. Thecircle
variable represents a Circle shape with a radius of 5.0, thesquare
variable represents a Square shape with a side length of 4.0, and thetriangle
variable represents a Triangle shape with side lengths 3.0, 4.0, and 5.0. -
The if let statement is used to destructure the circle instance, extracting the associated value (radius) if it is a Circle. If it is, it prints a message indicating that it's a circle with a specific radius.
Embracing the capability to define enums with variants that carry associated values is a powerful and versatile feature in Rust. This empowers us to encapsulate multiple types of values within a single type. Pretty cool, isn't it? But hold on, there's an additional layer of intrigue to explore!
rustenum Shape {Circle(f64), // Circle variant with radiusSquare(f64), // Square variant with side lengthTriangle(f64, f64, f64), // Triangle variant with three side lengths}impl Shape {fn area(&self) -> f64 {match self {Shape::Circle(radius) => std::f64::consts::PI * radius * radius,Shape::Square(side_length) => side_length * side_length,Shape::Triangle(a, b, c) => {let s = (a + b + c) / 2.0;(s * (s - a) * (s - b) * (s - c)).sqrt() // Heron's formula for triangle area}}}}fn main() {let circle = Shape::Circle(5.0);let square = Shape::Square(4.0);let triangle = Shape::Triangle(3.0, 4.0, 5.0);println!("Circle area: {}", circle.area()); // Circle area: 78.53981633974483println!("Square area: {}", square.area()); // Square area: 16println!("Triangle area: {}", triangle.area()); // Triangle area: 6}
Ok, that's a lot of code, let's break it down.
-
In Rust,
impl
is short for "implementation." It's used to define the methods associated with a particular type or trait. In the above example,impl Shape
is declaring the implementation block for the Shape enum. -
fn area(&self) -> f64 { ... }
Inside the impl block, there is a method calledarea
. This method takes a reference to self (an instance of Shape) and returns a floating-point number (f64). This method is what calculates the area based on the variant of theShape
enum. -
match self { ... }
The match statement is used to pattern match on the enum variant. Depending on whether the enum is a Circle, Square, or Triangle, it performs different calculations to determine the area.
So, in essence, the impl Shape
block allows you to define methods that operate on instances of the Shape
enum.
The area method, in particular, encapsulates the logic for calculating the area of each type of shape.
Now that's very cool!
I'm convinced that Rust's enums hold even more potential beyond my exploration. Just skimming the surface of Rust enums has revealed their remarkable power and enjoyable utility. I trust you found this post engaging. Until our next exploration!