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 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.

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:
  1. We create a range from 11 to numbernumber using the ..=..= range syntax.
  2. We call the for_eachfor_each method on the range. This method takes a closure as an argument and calls it for each element in the range.
  3. We construct a FizzBuzzFizzBuzz variant from each number using the FizzBuzz::new()FizzBuzz::new() function.
  4. We print the FizzBuzzFizzBuzz variant using the println!println! macro. The println!println! macro uses the DisplayDisplay trait to print values.

Extending FizzBuzz

Let's say we wanted to extend our FizzBuzz to support the following rules:
Extended FizzBuzz
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:
2024 · 서강대학교 미래교육원