Polyglot Dojo #2: FizzBuzz [2*]
Updates
[2019-12-23 Mon]: Added Rust Solution.
[2019-12-21 Sat]: Added Clojure Solution.
Challenge
This challenge was disappointingly dull, however, I saw some exciting things through the way. We are going to solve FizzBuzz, with the following constraints:
What is fizz buzz? Return the string representation of numbers from 1 to n
Multiples of 3 -> 'Fizz'
Multiples of 5 -> 'Buzz'
Multiples of 3 and 5 -> 'FizzBuzz'
Can we assume the inputs are valid? No
Can we assume this fits memory? Yes
And the provided unit test is as this:
from nose.tools import assert_equal, assert_raises
class TestFizzBuzz(object):
def test_fizz_buzz(self):
solution = Solution()
assert_raises(TypeError, solution.fizz_buzz, None)
assert_raises(ValueError, solution.fizz_buzz, 0)
expected = [ '1', '2', 'Fizz', '4', 'Buzz', 'Fizz', '7', '8', 'Fizz',
'Buzz', '11', 'Fizz', '13', '14', 'FizzBuzz' ]
assert_equal(solution.fizz_buzz(15), expected)
print('Success: test_fizz_buzz')
def main():
test = TestFizzBuzz()
test.test_fizz_buzz()
if __name__ == '__main__':
main()
So far, so good, let's start.
Python Solution
To solve this challenge, we first need to implement a function to take care of a single input (map an input to a single output). The constraints are so straightforward, and we basically need to re-write them in Python:
def is_divisible(number, by):
if by == 0:
raise ValueError("Expects non-zero denominator")
return number % by == 0
def process_number(number):
if is_divisible(number, 3) and is_divisible(number, 5):
return "FizzBuzz"
elif is_divisible(number, 3):
return "Fizz"
elif is_divisible(number, 5):
return "Buzz"
return str(number)
To make things a little bit more readable, I defined an is_divisible
function, which checks if two numbers are divisible, and used that function to translate our main constraints. If none of the rules applies, I return a string cast of the input number as the result.
With that part sorted out, I can use a list comprehension as follows, to produce my final result:
def fizz_buzz(self, number):
if not isinstance(number, int):
raise TypeError
elif number <= 0:
raise ValueError
return [self.process_number(n) for n in range(1, number + 1)]
First, I check for valid input and then used the range()
function inside a list comprehension to produce the final result, using our process_number
. Here is the solution in action:
$ open repl.it/@shahinism/FizzBuzz-Python โ
Now let's take a look at the repository's proposed solution. Here it is:
class Solution(object):
def fizz_buzz(self, num):
if num is None:
raise TypeError('num cannot be None')
if num < 1:
raise ValueError('num cannot be less than one')
results = []
for i in range(1, num + 1):
if i % 3 == 0 and i % 5 == 0:
results.append('FizzBuzz')
elif i % 3 == 0:
results.append('Fizz')
elif i % 5 == 0:
results.append('Buzz')
else:
results.append(str(i))
return results
The main logic used here is the same as mine, but there is a subtle difference which I like to talk about. From our constraints list, we know we can't assume that the input values are valid. So we need to implement the validation logic. In my solution, I ensure the input value is a non-zero integer, which given the task in hand (we expect to perform mathematical operations on the input, and we know the number list should grow by one) integers are quite a reasonable choice.
Yet in repository's solution, they are not ensuring it to be a number type, and rely on num is not None
. Here the function just expect it to be a value (and it can be anything other than None
and negative numbers), so a malicious call like fizz_buzz('five')
would easily pass our main constraints but fail due to wrong operation error. I usually prefer to limit input types to the minimum required, which is helpful in situations like this.
Clojure Solution
As usuall, we first need to translate our test suite to Clojure. The main test would be as simple as this:
(ns fizz-buzz-clj.core-test
(:require [clojure.test :refer :all]
[fizz-buzz-clj.core :refer :all]))
(deftest fizz-buzz-test
(testing "calculate the FizzBuzz of 15"
(is (= (fizzbuzz 15) ["1" "2" "Fizz" "4" "Buzz" "Fizz" "7" "8",
"Fizz" "Buzz" "11" "Fizz" "13" "14" "FizzBuzz"]))))
Yet, we haven't tested for invalid inputs as our constraints ask for. To cover this part of our main function, I plan to use Clojure's contract based programming facility by {:pre ...}
syntax. You can find more information about it from here. But basically, :pre
gets a set of condition describing the constraints of function's parameters, and on each application of the function, ensures, the provided arguments satisfy its condition. Otherwise, it'll throw an AssertionError
. So for this part of my unit tests, I can use this:
(testing "throws error on invalid inputs"
(is (thrown? AssertionError (fizzbuzz 0)))
(is (thrown? AssertionError (fizzbuzz nil))))
Quite straightforward, right? The main logic for my Clojure approach, is not much different from the Python solution, however, it contains, some satisfying syntax sugar ๐ . First I add a function to help me with divisibility check:
(defn is-divisible?
[number by]
{:pre [(not= by 0)]}
(= (mod number by) 0))
Here you can see the first contract I used which ensures the is-divisible?
function would get executed only if the value of by
is not equal to zero. Now I can use this function, to translate a number, to FizzBuzz:
(defn to-fizz-buzz
[number]
(cond
(and (is-divisible? number 5) (is-divisible? number 3)) "FizzBuzz"
(is-divisible? number 3) "Fizz"
(is-divisible? number 5) "Buzz"
:else (str number)))
Here I used a (cond ...)
expression to express the same logic as we had in Python sample. Now with our main logic implemented, we can write a function to satisfy our main test case:
(defn fizz-buzz
[number]
{:pre [(number? number) (> number 0)]}
(map to-fizz-buzz (range 1 (+ number 1))))
Again, you see the :pre
conditions to ensure the input values. The I use a map
to apply my to-fizz-buzz
function to a range of numbers based on the input number
. Now you can see the final result in action:
$ open repl.it/@shahinism/FizzBuzz-Clojure โ
Rust Solution
Now is the time to try our skills with Rust. Let's see how the test suite would look like:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fizzbuzz() {
assert_eq!(fizz_buzz(15), vec!["1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8",
"Fizz", "Buzz", "11", "Fizz", "13", "14", "FizzBuzz"]);
}
}
As you can see, here I don't test for invalid arguments like in Python or Clojure. Here I rely on Rust's type system, which would prevent passing invalid data types to the fizzbuzz function. Not is time to port my is_divisible
function to Rust. Here is the code:
fn is_divisible(number: i32, by: i32) -> bool {
number % by == 0
}
Rust distinguishes between recoverable and unrecoverable errors and provides different facilities to express each one of them. In this case, division by zero is a potential bug, and I could for example panic!
if the value of by
is equal to zero. Something like:
if by == 0 {
panic!("the value of by can't be equal to zero!");
}
However, I decided to not do that, since Rust itself would raise the same error, when you try to do that sort of crazy stuff. After all Rust supposed to keep us sane ๐:
Now I go forward and implement our main logic, using a match expression like this:
fn to_fizz_buzz<'a>(number: i32) -> String {
match number {
n if is_divisible(n, 3) && is_divisible(n, 5) => "FizzBuzz".to_string(),
n if is_divisible(n, 3) => "Fizz".to_string(),
n if is_divisible(n, 5) => "Buzz".to_string(),
n => n.to_string()
}
}
The logic here is quite straight forward, I check the value of number for different constraints, and return proper instance of String
as the result. Now I can use this function, to prepare my final result:
pub fn fizz_buzz(number: i32) -> Vec<String> {
let range: Vec<i32> = (1..number + 1).collect();
range.into_iter().map(to_fizz_buzz).collect()
}
In the first line of this function, I create a vector of i32
value types, containing the values I like to calculate FizzBuzz for. Then I .map
those values into to_fizz_buzz
function, and collect the final result. It's so close to the functional approach we took in Clojure and Python. Here you can find the final solution and play with it:
$ open repl.it/@shahinism/FizzBuzz-Rust โ
Conclusion
Finding solution for this challenge wasn't challenging at all. However, this provided me with the opportunity to experiment with each language's syntax a bit more and learn new stuff around them.