FizzBuzz in Rust

The FizzBuzz Test is sometimes given at interviews to filter out candidates who don’t program. The test is as follows:

Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”.

If you wanted to show that you can program, you’d write something like

fn main () {
    for n in 1..101 {
        if n % 15 == 0 {
            println!("FizzBuzz");
        } else if n % 3 == 0 {
            println!("Fizz");
        } else if n % 5 == 0 {
            println!("Buzz");
        } else {
            println!("{}", n);
        }
    }
}

This would be sufficient to get to the actual part of the interview where the interviewer can assume you can write a for loop. I’ll admit that half of the time I mess up if I’m not paying attention. This time I forgot to write the % 15 case first so I had to go back and fix it.

But let’s say you had to write this at your job for some reason. This solution is fine, until your boss comes back and says “OK, do the same thing, but now can you make it write Bazz instead of Buzz?”

It would be tempting to just make the substitution in place. But you have new knowledge that requirements may change frequently, so you decide to refactor the program before you change it.

fn main() {
    let first = "Fizz";
    let second = "Buzz";

    for n in 1..101 {
        println!("{}", fizzbuzz(first, second, n));
    }
    
}

fn fizzbuzz(first: &str, second: &str, n: i32) -> String {
    if n % 3 == 0 {
        first.to_string()
    } else if n % 5 == 0 {
        second.to_string()
    } else if n % 15 == 0 {
        first.to_string() + second
    } else {
        n.to_string()
    }
}

You can now freely change the string that’s being concatenated, it doesn’t matter if it’s Buzz or Bazz. You can even write a nice test for it:

#[cfg(test)]
mod tests {
    use super::fizzbuzz;

    #[test]
    fn it_works() {
        assert!(fizzbuzz("Fizz", "Buzz", 3) == "Fizz".to_string());
        assert!(fizzbuzz("Fizz", "Buzz", 5) == "Buzz".to_string());
        assert!(fizzbuzz("Fizz", "Buzz", 15) == "FizzBuzz".to_string());
        assert!(fizzbuzz("Fizz", "Buzz", 4) == 4.to_string());
    }
}

But now our boss comes back and says “What I meant is that you’re supposed to do Fizz, Buzz, Bazz for 3, 5, 7”. Suddenly, our solution is bad. Before we had four cases where we duplicate some code in one of them. Now we have nine cases, and if our boss comes back to ask for more features, possibly sixteen. We try this solution:

fn main() {
    let first = "Fizz";
    let second = "Buzz";

    for n in 1..101 {
        println!("{}", fizzbuzz(&[(first, 3), (second, 5)], n));
    }
}

pub fn fizzbuzz(tuples: &[(&str, i32)], n: i32) -> String {
    let mut output = String::from("");
    
    for &tuple in tuples {
        let (s, factor) = tuple;
        
        if n % factor == 0 {
             output = output + s;
        }
    }
    
    if output == "" {
        n.to_string()
    } else {
        output
    }
}

It’s a clever hack, exploiting the fact that the empty string is an identity element in string concatenation. Unfortunately, it doesn’t pass our new test suite:

#[cfg(test)]
mod tests {
    use super::fizzbuzz;

    #[test]
    fn it_works() {
        assert!(fizzbuzz(&[("Fizz", 3), ("Buzz", 5)], 3) == "Fizz".to_string());
        assert!(fizzbuzz(&[("Fizz", 3), ("Buzz", 5)], 5) == "Buzz".to_string());
        assert!(fizzbuzz(&[("Fizz", 3), ("Buzz", 5)], 15) == "FizzBuzz".to_string());
        assert!(fizzbuzz(&[("Fizz", 3), ("Buzz", 5)], 4) == 4.to_string());
        assert!(fizzbuzz(&[("", 3), ("Buzz", 5)], 3) == "".to_string());
    }
}

Because we used arbitrary strings, our function doesn’t work for empty strings! I don’t know about you, but I feel uneasy writing a function that fails on some valid inputs. Even if we think (read: hope) that those inputs won’t be fed into our function, we can do better. Rust helpfully has the Option type to actually mark absence of something. We can mark the absence of a match as None, and the presence of a match as Some(String).

pub fn fizzbuzz(tuples: &[(&str, i32)], n: i32) -> String {
    let default = || n.to_string();
    accumulate(tuples, n).unwrap_or_else(default)
}

Now we’ll write the accumulate function. There is actually a library called itertools that has a function called fold1 that does a fold starting with None and returns Some if there are things to fold.

fn accumulate(tuples: &[(&str, i32)], n: i32) -> Option<String> {
    tuples.iter()
        .filter(|&x|n % x.1 == 0)
        .map(first)
        .cloned()
        .map(<&str>::into)
        .fold1(String::concat)
}

fn first<A, B>(&(ref first, _): &(A, B)) -> &A {
    first
}

The function first is not really necessary, but it feels clearer to me. We also have to unfortunately write a trait for things that can be concatenated.

pub trait Concatenable {
    fn concat(self, other: Self) -> Self;
}

impl Concatenable for String {
    fn concat(mut self, other: String) -> String {
        self.push_str(&*other);
        self
    }
}

But we’ll ignore that and hope Add is implemented on String + String in the future so we can easily call fold1 on it. But notice that our FizzBuzz program no longer has a single if statement or even a match. This is because the logic of excluding non-factors is inside the filter and the logic for writing the original number is in unwrap_or_else. The benefit of this is there is only one path through the program. The whole program is now in a declarative style.

The final result (with some dependencies inlined) is here: https://is.gd/QVal2B

I also wrote the same thing using Cow<'a, str> and each condition being a closure in my repository:

https://bitbucket.org/iopq/fizzbuzz-in-rust