Solving Fizz Buzz computationally (No conditional logic)! — Yet we end up philosophizing about optimization.

Hristo Gueorguiev
Jul 21, 2019 · 6 min read

First off I suppose let’s define what FizzBuzz is. It’s a computer programming problem that was commonly (or so I hear) used in tech interviews to asses the individuals ability to convert problems into computer code solutions.

Sometimes criticized by its detractors for being too simple, and/or not really representative of the actual requirements in a programmers job.

While seen by others as a quick way to filter out candidates that lack basic programmatic problem solving.

All it asks for is, when given a number to return Fizz if the number is divisible by 3, Buzz if it’s divisible by 5, FizzBuzz if divisible by both and the original number if not any of those two.

Since somehow this ends up being side tracked into a discussion about optimization before we go any further lets drop the interactive code we will be discussing right here.

Select data set size with the slider and click ‘Compute!’ to see run times chart for all solvers.

OK, let’s start by taking a look at the code now.

Here we have the classic Fizz Buzz solver that uses if statements to check for remainder of the division and based on that the function returns the desired answer.

function FBSolve_Standard(number){
if ( (number%5 == 0) && (number%3 == 0) ) return ‘FizzBuzz’;
if ( number%3 == 0) return ‘Fizz’;
if ( number%5 == 0) return ‘Buzz’;
return number;
}

Bellow is the solver that uses no conditional logic to solve the Fizz Buzz question, we get pretty cheeky with some implicit conversions which we use to obtain indexes into our answer map array (answerkey = [‘’, ‘Fizz’, ‘Buzz’, 0];)

function FBSolve_Map(number, answerkey){

answer = answerkey[0+!((number%3)/(number%3))] +
answerkey[(!((number%5)/(number%5)) + 0)*2] +
answerkey[!(!((number%5)/(number%5)) || !((number%3)/(number%3))) * 3];

return answer;
}

How does it work …

Well first we create an array (list) that contains the answers, we have an empty string, ‘Fizz’, ‘Buzz’ and also we have to populate the last element with the current number we are assessing.

We use some silly math and implicit conversions from numbers to Boolean and back to a numeric type to calculate indexes in the answer array that would correspond to the correct answer based on the number to assess being divisible by 3, 5, both or none. We then string concatenate the answers from the array to render the complete answer.

Now as you can tell this code is not optimized at all since we repeat the modulo operation a number of unnecessary times, we could have simply cashed those values above. However regardless of that even with them cashed there is still a ton of conversions and mathematical operations that need to be performed for every number we check. That’s not even including the string processing to render the final answer.

Even more interesting it seems when we implement a version of this that caches those values we actually end up performing worse, which suggests that the JavaScript engine is doing a great job of optimizing and cashing them already and all we end up doing by explicitly cashing is adding overhead.

function FBSolve_Map_Optimized(number, answerkey){ let mod3 = !((number%3)/(number%3));
let mod5 = !((number%5)/(number%5));
answer = answerkey[0+mod3] + answerkey[(mod5 + 0)*2] + answerkey[!(mod5 || mod3) * 3]; return answer;}

We could attempt fix our cashing by using global variables for example so they don’t have to be reallocated on function calls or something like it. Regardless we would be trying to outrun the optimizations that the JavaScript engine is performing and it is in much better position than us to make those decisions most of the time.

Plain to see that the basic conditional logic solution can exit and return an answer as soon a statement is satisfied, which in turn makes it much more performant in a lot of cases.

As an added twist when the number assessed is not divisible by 3 or 5, we return it, but since our function returns string in all other cases we should convert that to string as well. Of course that adds overhead to our standard solver since it was not doing that work before, unlike our map solvers that were already performing that implicit coercion. The standard solver still performs better but not as well as before, naturally.

If we now add a fourth kind of solver, one that only has a single exit out of the function but still uses conditional logic.

function FBSolve_Ternary(number){
answer=’’;
if(!(number%3)) answer = ‘Fizz’;
if(!(number%5)) answer += ’Buzz’;
return !answer? number: answer.toString();
}

Let’s call this our Ternary solver, it ends up performing almost as good as the standard solver when we are NOT doing string conversion on returning the number, and performs even better than the standard solver when we do.

How is does it end up beating our standard solver, since we are still executing more lines of code on average in the Ternary solver? Well this is just a guess, maybe the JS runtime doesn’t go ahead and pre-compute the costly string coercion in the Ternary solver but does so in the Standard one ?!

You can experiment with different cases by changing conditions in the repl embedded above. Changing the data set size, the max number to be computed, where we coerce to string or not etc.

If you find out let me know!

Computation times and the deltas between computation times for the different solvers change with increase or decrease in data size, so they operate in different big O run times. Which can lead us nicely in to a few topics.

First just because something seems clever it doesn’t mean it will outperform a simpler solution, in a case like this, that is more or less obvious but even here a few unexpected results come up. In real world cases the trap of over engineering can be quite tempting as engineers like solving problems in cleaver ways. But as we know simpler is often better, and optimizing early is absolutely a trap, we just need to remind ourselves of that every once in a while.

Optimization should be case specific and measured, and there should be good reason for it, otherwise it’s not only waste of time and resources that could have gone in new feature creation but it could actually make things slower.

This quote captures the sentiment quite beautifully.

“ The competent programmer is fully aware of the strictly limited size of his own skull; therefore he approaches the programming task in full humility, and among other things he avoids clever tricks like the plague.” — Dijkstra (1972) The Humble Programmer

Second-ly … we can see how dogmatic following of rules, ex. “… having single exit out of a block …” or “… computation is always preferred to conditional logic …”, can be impractical. As such programming best practices and rules, just like paradigms, methodologies, architecture models etc. are simply tools to be applied to problems where they fit and can provide us with the desired benefits for the specific case. With a well rounded understanding of variety of said tools, problems can be solved in simple, robust and efficient ways … without the need to over engineer, reinvent or waste valuable time following a cannon in a religious manner for no other reason than dogma.

Now I know full well these things have been stated and overstated many a times … but seems to me they can’t be said too many times, so there you go.

Plus solving FizzBuzz in silly ways is kind of like planking for programmers, something to post on social media for attention!

Type to you all next time, and remember the answer sometimes is simply 0x90.

-Hristo.

Hristo Gueorguiev

Written by

Programmer, tinkerer. Technology, Philosophy, Finance/Econ, Fitness. HristoGueorguiev.com

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade