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
enum
enum
s and trait
trait
s and play around with match
match
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
Result
Result
and Option
Option
enums. If you're not, don't worry, we'll go over them right now.The
Result
Result
enum is used to represent the result of a function that may fail. It has two variants: Ok
Ok
and Err
Err
.enum Result<T, E> {
Ok(T),
Err(E),
}
enum Result<T, E> {
Ok(T),
Err(E),
}
When a function returns a
Result
Result
, 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
match
match
expression. This is similar to a switch
switch
statement in TypeScript, but it's more powerful. match
match
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
Option
Option
enum is similar to Result
Result
, but it's used to represent the possibility of a value. It has two variants: Some
Some
and None
None
.enum Option<T> {
Some(T),
None,
}
enum Option<T> {
Some(T),
None,
}
While
Result
Result
is used to represent the possibility of an error, Option
Option
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
Animal
Animal
that has a method called speak
speak
.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
Dog
Dog
can call the speak
speak
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
1
1
to the given number
number
, 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
FizzBuzz
FizzBuzz
enum. This enum will have 4 variants: Fizz
Fizz
, Buzz
Buzz
, FizzBuzz
FizzBuzz
, and Number
Number
.enum FizzBuzz {
Fizz,
Buzz,
FizzBuzz,
Number(u32),
}
enum FizzBuzz {
Fizz,
Buzz,
FizzBuzz,
Number(u32),
}
Note
The
Number
Number
variant is associated with the u32
u32
type. This means that FizzBuzz::Number()
FizzBuzz::Number()
is a function that constructs an instance of the Number
Number
variant that holds a u32
u32
value. We could have called this variant however we wanted.Next, we'll associate a method called
new
new
with the FizzBuzz
FizzBuzz
enum. This method will take a number and return the appropriate FizzBuzz
FizzBuzz
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 FizzBuzz
FizzBuzz
. For example, if number
number
is 15
15
, then the tuple will be (true, true)
(true, true)
because 15
15
is divisible by both 3
3
and 5
5
. This will match the first arm of the match
match
expression, which will return the FizzBuzz::FizzBuzz
FizzBuzz::FizzBuzz
variant.Now that we have a way to construct
FizzBuzz
FizzBuzz
variants, we need a way to print them. We'll do this by implementing the Display
Display
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
Display
Display
trait is used to print values. The fmt
fmt
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
1
1
tonumber
number
using the..=
..=
range syntax. - We call the
for_each
for_each
method on the range. This method takes a closure as an argument and calls it for each element in the range. - We construct a
FizzBuzz
FizzBuzz
variant from each number using theFizzBuzz::new()
FizzBuzz::new()
function. - We print the
FizzBuzz
FizzBuzz
variant using theprintln!
println!
macro. Theprintln!
println!
macro uses theDisplay
Display
trait 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
FizzBuzz
FizzBuzz
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
Display
Display
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.