FizzBuzz: The Rusty Way
Published
· 2 years ago
🦀rustfizzbuzzenumstraits
In this article we'll be implementing FizzBuzz in Rust
We'll start with a naive implementation, and then we'll refactor it to make it more reusable. Along the way, we'll learn about Rust's
🦀We'll start with a naive implementation, and then we'll refactor it to make it more reusable. Along the way, we'll learn about Rust's
enumenums and traittraits and play around with matchmatch expressions, while using TypeScript for comparison.Note
I'm still learning Rust, so if you see any mistakes or have any suggestions, please let me know in the comments!
Enums in Rust
One thing that stood out to me when learning Rust, was that unlike TypeScript enums, Rust enums are acutally useful.
If you're already familiar with Rust, you probably know Rust's
ResultResult and OptionOption enums. If you're not, don't worry, we'll go over them right now.The
ResultResult enum is used to represent the result of a function that may fail. It has two variants: OkOk and ErrErr.enum Result<T, E> {
Ok(T),
Err(E),
}enum Result<T, E> {
Ok(T),
Err(E),
}When a function returns a
ResultResult, whoever calls it knows they should handle both cases. This is a great way to ensure that errors are handled, and that the program doesn't just crash. In contrast, TypeScript doesn't have a way to enforce error handling, or even to represent the possibility of an error in the type system, so it all comes down to developer discipline and documentation.One way to handle enums is to use a
matchmatch expression. This is similar to a switchswitch statement in TypeScript, but it's more powerful. matchmatch is exhaustive, which means that you have to handle every case, and the compiler will yell at you if you don't.// return type error type
// \ /
fn may_fail(x: i32) -> Result<i32, ()> {
if x !== 0 {
Ok(x)
} else {
Err(())
}
}
fn main() {
match may_fail(42) {
// If the function returns the Ok variant,
// we unwrap the value inside and call it n.
// This is similar to destructuring in TypeScript.
Ok(n) => println!("Success: {}", n),
// In our case, if the function returns the Err variant,
// we don't need to bind it to a variable, so we use _
Err(_) => println!("Failure"),
}
}// return type error type
// \ /
fn may_fail(x: i32) -> Result<i32, ()> {
if x !== 0 {
Ok(x)
} else {
Err(())
}
}
fn main() {
match may_fail(42) {
// If the function returns the Ok variant,
// we unwrap the value inside and call it n.
// This is similar to destructuring in TypeScript.
Ok(n) => println!("Success: {}", n),
// In our case, if the function returns the Err variant,
// we don't need to bind it to a variable, so we use _
Err(_) => println!("Failure"),
}
}The
OptionOption enum is similar to ResultResult, but it's used to represent the possibility of a value. It has two variants: SomeSome and NoneNone.enum Option<T> {
Some(T),
None,
}enum Option<T> {
Some(T),
None,
}While
ResultResult is used to represent the possibility of an error, OptionOption is used to represent the possibility of a missing value.// return type
// |
fn get_first_element(arr: &[i32]) -> Option<i32> {
if arr.len() > 0 {
Some(arr[0])
} else {
None
}
}
fn main() {
let arr = [1, 2, 3];
match get_first_element(&arr) {
// If the function returns the Some variant,
// we unwrap the value inside and call it n.
Some(n) => println!("Success: {}", n),
// The None variant doesn't hold a value.
None => println!("Failure"),
}
}// return type
// |
fn get_first_element(arr: &[i32]) -> Option<i32> {
if arr.len() > 0 {
Some(arr[0])
} else {
None
}
}
fn main() {
let arr = [1, 2, 3];
match get_first_element(&arr) {
// If the function returns the Some variant,
// we unwrap the value inside and call it n.
Some(n) => println!("Success: {}", n),
// The None variant doesn't hold a value.
None => println!("Failure"),
}
}Later, we will create our own enum to represent the possible outputs of FizzBuzz. But first, let's talk about traits.
Traits in Rust
Traits are similar to interfaces in TypeScript. They're used to define shared behavior between types. For example, we can define a trait called
AnimalAnimal that has a method called speakspeak.trait Animal {
fn speak(&self);
}trait Animal {
fn speak(&self);
}We can then implement this trait for any type we want.
struct Dog;
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}struct Dog;
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}Now, any instance of
DogDog can call the speakspeak method.let dog = Dog;
dog.speak(); // Woof!let dog = Dog;
dog.speak(); // Woof!Note
Rust allows you implement native traits for your custom types, as well as custom traits for native types, which I think is pretty cool.
You can also implement traits for enums, which we'll do later.
You can also implement traits for enums, which we'll do later.
FizzBuzz: The Naive Way
Let's forget everything we talked about and try to implement FizzBuzz naively.
fn fizzbuzz(number: u32) {
for x in 1..=number {
if x % 3 == 0 && x % 5 == 0 {
println!("FizzBuzz");
} else if x % 3 == 0 {
println!("Fizz");
} else if x % 5 == 0 {
println!("Buzz");
} else {
println!("{}", i);
}
}
}fn fizzbuzz(number: u32) {
for x in 1..=number {
if x % 3 == 0 && x % 5 == 0 {
println!("FizzBuzz");
} else if x % 3 == 0 {
println!("Fizz");
} else if x % 5 == 0 {
println!("Buzz");
} else {
println!("{}", i);
}
}
}This is pretty straightforward. We loop through the numbers from
11 to the given numbernumber, and print the appropriate string. However, it's not very reusable. What if we wanted to print "FooBuzz""FooBuzz" when the number is divisible by 5 and 7 but not 3? Our code would get messy pretty quickly.Now let's try to make our FizzBuzz more reusable by introducing enums and traits.
FizzBuzz: The Rusty Way
Let's start by creating a
FizzBuzzFizzBuzz enum. This enum will have 4 variants: FizzFizz, BuzzBuzz, FizzBuzzFizzBuzz, and NumberNumber.enum FizzBuzz {
Fizz,
Buzz,
FizzBuzz,
Number(u32),
}enum FizzBuzz {
Fizz,
Buzz,
FizzBuzz,
Number(u32),
}Note
The
NumberNumber variant is associated with the u32u32 type. This means that FizzBuzz::Number()FizzBuzz::Number() is a function that constructs an instance of the NumberNumber variant that holds a u32u32 value. We could have called this variant however we wanted.Next, we'll associate a method called
newnew with the FizzBuzzFizzBuzz enum. This method will take a number and return the appropriate FizzBuzzFizzBuzz variant.impl FizzBuzz {
fn new(number: u32) -> FizzBuzz {
match (number % 3 == 0, number % 5 == 0) {
(true, true) => FizzBuzz::FizzBuzz,
(true, false) => FizzBuzz::Fizz,
(false, true) => FizzBuzz::Buzz,
(false, false) => FizzBuzz::Number(number),
}
}
}impl FizzBuzz {
fn new(number: u32) -> FizzBuzz {
match (number % 3 == 0, number % 5 == 0) {
(true, true) => FizzBuzz::FizzBuzz,
(true, false) => FizzBuzz::Fizz,
(false, true) => FizzBuzz::Buzz,
(false, false) => FizzBuzz::Number(number),
}
}
}We use pattern matching to match the tuple
(number % 3 == 0, number % 5 == 0)(number % 3 == 0, number % 5 == 0) to the different variants of FizzBuzzFizzBuzz. For example, if numbernumber is 1515, then the tuple will be (true, true)(true, true) because 1515 is divisible by both 33 and 55. This will match the first arm of the matchmatch expression, which will return the FizzBuzz::FizzBuzzFizzBuzz::FizzBuzz variant.Now that we have a way to construct
FizzBuzzFizzBuzz variants, we need a way to print them. We'll do this by implementing the DisplayDisplay trait.
We can bring this trait to scope by adding use std::fmt::{Display, Formatter, Result};use std::fmt::{Display, Formatter, Result}; to the top of our file.impl Display for FizzBuzz {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
FizzBuzz::Fizz => write!(f, "Fizz"),
FizzBuzz::Buzz => write!(f, "Buzz"),
FizzBuzz::FizzBuzz => write!(f, "FizzBuzz"),
FizzBuzz::Number(number) => write!(f, "{}", number),
}
}
}impl Display for FizzBuzz {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
FizzBuzz::Fizz => write!(f, "Fizz"),
FizzBuzz::Buzz => write!(f, "Buzz"),
FizzBuzz::FizzBuzz => write!(f, "FizzBuzz"),
FizzBuzz::Number(number) => write!(f, "{}", number),
}
}
}The
DisplayDisplay trait is used to print values. The fmtfmt method is called when we use the {}{} placeholder in a println!println! macro.We can now write our FizzBuzz function as follows:
fn fizzbuzz(number: u32) {
(1..=number).for_each(|x| println!("{}", FizzBuzz::new(x)))
}fn fizzbuzz(number: u32) {
(1..=number).for_each(|x| println!("{}", FizzBuzz::new(x)))
}Very clean! Let's break down what's happening here:
- We create a range from
11tonumbernumberusing the..=..=range syntax. - We call the
for_eachfor_eachmethod on the range. This method takes a closure as an argument and calls it for each element in the range. - We construct a
FizzBuzzFizzBuzzvariant from each number using theFizzBuzz::new()FizzBuzz::new()function. - We print the
FizzBuzzFizzBuzzvariant using theprintln!println!macro. Theprintln!println!macro uses theDisplayDisplaytrait to print values.
Extending FizzBuzz
Let's say we wanted to extend our FizzBuzz to support the following rules:

A green cell means that the number is divisible by the corresponding number, and the rightmost column indicates the string to print. For example, if the number is not divisible by 3, 5, or 7, we print the number itself. If the number is divisible by 3 and 7 but not 5, we print
"FizzFoo""FizzFoo".Let's update our
FizzBuzzFizzBuzz enum to include the 4 new variants.enum FizzBuzz {
Foo,
Bar,
Fizz,
Buzz,
FizzBuzz,
FooBuzz,
FizzFoo,
Number(u32),
}enum FizzBuzz {
Foo,
Bar,
Fizz,
Buzz,
FizzBuzz,
FooBuzz,
FizzFoo,
Number(u32),
}Then, we need to update the
FizzBuzz::new()FizzBuzz::new() method to return the appropriate variant.impl FizzBuzz {
fn new(number: u32) -> FizzBuzz {
match (number % 3 == 0, number % 5 == 0, number % 7 == 0) {
(true, true, true) => FizzBuzz::Bar,
(true, true, false) => FizzBuzz::FizzBuzz,
(true, false, true) => FizzBuzz::FizzFoo,
(true, false, false) => FizzBuzz::Fizz,
(false, true, true) => FizzBuzz::FooBuzz,
(false, true, false) => FizzBuzz::Buzz,
(false, false, true) => FizzBuzz::Foo,
(false, false, false) => FizzBuzz::Number(number),
}
}
}impl FizzBuzz {
fn new(number: u32) -> FizzBuzz {
match (number % 3 == 0, number % 5 == 0, number % 7 == 0) {
(true, true, true) => FizzBuzz::Bar,
(true, true, false) => FizzBuzz::FizzBuzz,
(true, false, true) => FizzBuzz::FizzFoo,
(true, false, false) => FizzBuzz::Fizz,
(false, true, true) => FizzBuzz::FooBuzz,
(false, true, false) => FizzBuzz::Buzz,
(false, false, true) => FizzBuzz::Foo,
(false, false, false) => FizzBuzz::Number(number),
}
}
}Notice how our match expression is derived directly from the rules table. This is a good example of how Rust's pattern matching can be used to make code more readable.
Lastly, we need to update the
DisplayDisplay trait implementation to print the new variants.use std::fmt::{Display, Formatter, Result};
impl Display for FizzBuzz {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
FizzBuzz::Foo => write!(f, "Foo"),
FizzBuzz::Bar => write!(f, "Bar"),
FizzBuzz::Fizz => write!(f, "Fizz"),
FizzBuzz::Buzz => write!(f, "Buzz"),
FizzBuzz::FizzBuzz => write!(f, "FizzBuzz"),
FizzBuzz::FooBuzz => write!(f, "FooBuzz"),
FizzBuzz::FizzFoo => write!(f, "FizzFoo"),
FizzBuzz::Number(number) => write!(f, "{}", number),
}
}
}use std::fmt::{Display, Formatter, Result};
impl Display for FizzBuzz {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
FizzBuzz::Foo => write!(f, "Foo"),
FizzBuzz::Bar => write!(f, "Bar"),
FizzBuzz::Fizz => write!(f, "Fizz"),
FizzBuzz::Buzz => write!(f, "Buzz"),
FizzBuzz::FizzBuzz => write!(f, "FizzBuzz"),
FizzBuzz::FooBuzz => write!(f, "FooBuzz"),
FizzBuzz::FizzFoo => write!(f, "FizzFoo"),
FizzBuzz::Number(number) => write!(f, "{}", number),
}
}
}And we're done! We've implemented an extensible FizzBuzz function that can be modified to support any number of rules.
If you're trying to get into Rust, check out the following resources:
- Rust Book with quizzes - The official Rust book, modified to include quizzes.
- Rust, how do I start? - Hand curated advice and pointers for getting started with Rust.
- rust-learning - A bunch of links to blog posts, articles, videos, etc for learning Rust.