Updates

[20191223 Mon]: Added Rust Solution.

[20191221 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 rewrite them in Python:
def is_divisible(number, by):
if by == 0:
raise ValueError("Expects nonzero 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/FizzBuzzPython █
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 nonzero 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 fizzbuzzclj.coretest
(:require [clojure.test :refer :all]
[fizzbuzzclj.core :refer :all]))
(deftest fizzbuzztest
(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 isdivisible?
[number by]
{:pre [(not= by 0)]}
(= (mod number by) 0))
Here you can see the first contract I used which ensures the isdivisible?
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 tofizzbuzz
[number]
(cond
(and (isdivisible? number 5) (isdivisible? number 3)) "FizzBuzz"
(isdivisible? number 3) "Fizz"
(isdivisible? 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 fizzbuzz
[number]
{:pre [(number? number) (> number 0)]}
(map tofizzbuzz (range 1 (+ number 1))))
Again, you see the :pre
conditions to ensure the input values. The I use a
map
to apply my tofizzbuzz
function to a range of numbers based on the
input number
. Now you can see the final result in action:
$ open repl.it/@shahinism/FizzBuzzClojure █
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/FizzBuzzRust █
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.