blester125 / fizzbuzz

A fizzbuzz implementation without double checking.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

FizzBuzz without Rechecking

What is FizzBuzz

Proported to be a classic interview question to filter out people who do not know how to code, although I have never known anyone who actually has been asked to do this.

Given some number $n$, print all numbers between 1 and $n$ except if a number is divisible by $3$ it should be replaced by Fizz, if it is divisible by 5 it should be replaced as Buzz, and if it is divisible by both it should be replaced with FizzBuzz.

Two Classic Solutions

Below are two of the common solutions. Note: They are shown as a function that takes a single integer, and returns string to output as the outer loop to print all the numbers from $1 → n$ is rather trivial and this function is there the true differences are.

Building it Up

One solution is to build up a result of Fizz and Buzz and return that (or the integer).

def fizzbuzz(n: int) -> str:
    result = ""
    if n % 3 == 0:
        result += "Fizz"
    if n % 5 == 0:
        result += "Buzz"
    if not result:
        result = str(n)
    return result

Checking all Cases

A second solution is to explicitly check the “divisible by $3$ and $5$” check (represented here in the % 15 check as 15 is the Least Common Multiple of 3 and 5).

def fizzbuzz(n: int) -> str:
    if n % 15 == 0:
        return "FizzBuzz"
    if n % 3 == 0:
        return "Fizz"
    if n % 5 == 0:
        return "Buzz"
    return str(n)

Double Checking

Both the solutions above result in doing a double check. In the “Build it Up” solution, we double check with the if not result. We are checking if neither the mod $3$ or the mod $5$ checks passed. If neither of them did we execute the default action and return the current number. In the second solution we are double checking the divisible by 3 and divisibility or 5 between the 15 branch and the 3 and 5 branches.

Additionally, we can see how expanding this solution to include more checks, for example FizzBuzzHissHowl, using the numbers $3, 5, 7$ and $9$, would cause an explosion in complexity.

Single Checking

The question then becomes, can we solve FizzBuzz without doing this double check? As summarized in this write-up of solving FizzBuzz using a domain specific language in Haskell, the interesting part of FizzBuzz is that the control flow required is such that “the default action is executed only if some previous actions were not executed.” That is our double checking above, it is especially clear in the “Build it Up” solution where we only trigger the default of neither of the other cases fired.

Our Functions

def fizzbuzz(n: int) -> str:
    def test(d: int, s: str, f: Callable[..., str]) -> Callable[..., str]:
        if n % d == 0:
            return lambda _: s + f("")
        return f

    fizz = functools.partial(test, 3, "Fizz")
    buzz = functools.parital(test, 5, "Buzz")
    return fizz(buzz(lambda s: s))(str(n))

The above solution (modified from this video) solves fizzbuzz without double checking and is easy to extend to more multiples as seen in fizzbuzz.py. The version here also uses inner functions to make the solution more self-contained.

In our solution, we encode this with functions. At the base case we have our default action lambda s: s returning the number as is. Then we build up a chain of functions, each one representing a divisibility check. If a check is successful, we return a function that, when called, will add the string representing this check passing (Fizz, Buzz, etc.) to the output of the of the calling the function the original check as passed. The critical part of this is that the this call passes the empty string to the function, effectively turning it off if it is the default action. If the check is not successful, the pass in function is return, meaning that if this check doesn’t pass, the default function will eventually be called with the number and execute the base case.

Let’s examine what happens when a number is, not divisible by $3$ or $5$, is divisible by $3$, is divisible by $5$, and is divisible by both.

N = 2

Starting with the case of not divisible by either, we have our default action lambda s: s. When this is called with ~”2”~ we will get our number. First we look at buzz(lambda s: s). The check 2 % 5 =​= 0 happens, and if fails meaning that the default action is returned. This default action is then the one passed to fizz. Inside fizz, the check 2 % 3 == 0 also fails. This results in the default action being returned so the final result of fizz(buzz(lambda s: s)) is just lambda s: s so when we finally call it with str(2) we get 2 back.

N = 3

In this case where $n$ is divisible by $3$ but not $5$, we start with our default action and pass that as f to buzz. The check for divisibility by $5$ fails so the returned function is the default action. This is then passed to fizz. Inside the fizz function, the 3 % 3 =​= 0 check passes. This means the returned function is our new lambda. This lambda will return the string =”fizz”= plus the result of calling f with the empty string. f is our default action so the return value is ~”​”~ (effectively turning off the default action). and the final result is ~”Fizz”~​.

N = 5

When $n$ is only divisible by $5$, the call to buzz with the default action return a new function that when called will return ~”Buzz”~ plus the result of the default action called with ~”​”~ (which is again means the default is turned off and final result is just a ~”Buzz”​). This function that will return ~"Buzz"~ is passed to ~fizz~​. The ~fizz check fails returning the buzz returning function and a final call will output ~”Buzz”~​.

N = 15

In the final case where $n$ is divisible by both $3$ and $5$, the first call to buzz(lambda s: s) will return a function that outputs ~”Buzz”​. Then the call to ~fizz will return a function that returns ~”Fizz”~ to the result of the input function f called with the empty string. In this case, f is not the default action but instead one that returns ~”Buzz”~ plus the turned off default action. So the final result when called returns ~”FizzBuzz”~​.

Extensions

By adding extra check functions (in the order you want the words to appear in the output string) to this call chain it is easy to extend FizzBuzz to any number of divisibility checks without an explosion in case logic or extra checks.

About

A fizzbuzz implementation without double checking.


Languages

Language:Python 100.0%