6
\$\begingroup\$

I was talking to a coworker about over engineering things, which somehow lead to me over engineering the hell out of fizzbuzz. I went for a functional recursive solution in JavaScript. What do you think?

Full disclosure: my knowledge of functional programming begins and ends at "functions are king" and "no side effects" so I would LOVE some feedback regarding functional programming. But please point out other inefficiencies as well.

fizzbuzz();

function fizzbuzz() {
    var i = 1;
    var result = [];

    (function loop() {
        var str = '';
        str += fizz(i);
        str += buzz(i);

        result.push(ifFalsy(str, i));

        if (isLt100(i++)) {
            loop();
        }
    })();

    print(result.join(', '));
}

function fizz(num) {
    return isDivBy3(num) ? 'fizz' : '';
}

function buzz(num) {
    return isDivBy5(num) ? 'buzz' : '';
}

function isDivBy3(num) {
    return num % 3 === 0;
}

function isDivBy5(num) {
    return num % 5 === 0;
}

function isLt100(num) {
    return num < 100;
}

function ifFalsy(value, fallback) {
    return !value ? fallback : value;
}

function print(str) {
    console.log(str);
}
\$\endgroup\$
8
\$\begingroup\$

Functional programming is not about writing as many functions as you can. I would suggest to start with a function library (e.g. Ramda) and write as few new functions as needed, taking advantage of currying.

Obviously, your loop function has side effects: it modifies result and i.

Without going to deep into FP rabbit hole, your function could look like this:

function getFizzBuzzArray() {
    function getFizzBuzz(v) {
        var ret = R.concat(
          v % 3 === 0 ? 'fizz' : '',
          v % 5 === 0 ? 'buzz' : ''
        );

        return ret || v;
    }
  
    return R.map(getFizzBuzz, R.range(1, 101));
}

document.write(getFizzBuzzArray());
<script src="https://cdn.jsdelivr.net/ramda/0.18.0/ramda.min.js"></script>

\$\endgroup\$
  • 2
    \$\begingroup\$ And if you are going to look at at using a library like Ramda, you might want to look at another review that takes on this same question and seems to have the definitive Ramda solution. \$\endgroup\$ – Scott Sauyet Dec 2 '15 at 18:52
7
\$\begingroup\$

First, isLt100, isDivBy3 and your other functions are vaguely named. Better name them fully and meaningfully. There's no harm done with the extra keystrokes in the name.

Looking at your fizzbuzz, it appears that loop is impure as it is mutating something from the outside of the function. You're doing a push inside loop to result which is outside the loop. Also, you're mutating i as well.

Also, you would want to make your range flexible. Now, as far as I know, functional programming fans prefer a "range" function to iterate through. This makes it easy for iteration functions like reduce, map, filter etc. to go through. Since there's no such thing in JS, this SO answer provides us one. It's essentially just creating an array of n length.

Next is your fizzbuzz. Since we have an array of numbers thanks to range, all we need to do now is just map these values with numbers, fizz, buzz or fizz buzz. We can use the native map array method to do that. We just need to provide the number to fizzBuzzTest and return it's result to map to create our new array of numbers, fizz, buzz or fizz buzz.

Warning: I'm using ES6 syntax. Run snippet in browser that supports at least arrow functions.

function range(n){
  return Array.apply(null, Array(n)).map((_, i) => i);
}

function fizzBuzzTest(n){
  var by3 = n % 3 === 0;
  var by5 = n % 5 === 0;

  return  by3 && by5 ? 'fizz buzz'
               : by3 ? 'fizz'
               : by5 ? 'buzz'
                     : n;
}

function fizzBuzz(n){
  return range(n).map(x => fizzBuzzTest(x + 1)).join(', ');

  // A non-OOP approach would have the same, except function calls are nested rather
  // than chained (Python)
  // return ','.join(map(lambda x: fizzBuzz(x + 1), range(100)));
}

// SE really needs something elegant to print stuff with on snippets
document.write(fizzBuzz(100));

\$\endgroup\$
  • \$\begingroup\$ +1 for the functional approach using ES6. Thanks for using fat arrows to make the code cleaner :) \$\endgroup\$ – Zorgatone Dec 2 '15 at 10:03
  • 1
    \$\begingroup\$ range(n).map(x => fizzBuzzTest(x + 1)) could simplified to Array(n).fill().map((_, i) => fizzBuzzTest(i + 1)) \$\endgroup\$ – Pavlo Dec 2 '15 at 20:51
  • \$\begingroup\$ @Pavlo Yup, one of those new things I don't know about yet. :D \$\endgroup\$ – Joseph Dec 2 '15 at 20:58
  • 1
    \$\begingroup\$ range can be simplified to const range = n => [...Array(n).keys()];, using more ES6 goodies. \$\endgroup\$ – Scott Dec 3 '15 at 18:03
0
\$\begingroup\$

There is a way to write "pure functional" FizzBuzz, without accumulating or checking the condition twice. Since all of the answers so far use 2+ if statements, here's my version:

function fizzbuzz(n) {
  const test = (divisor, callout, f) => (n % divisor === 0) ?
    () => callout + f('') :
    f;
  const fizz = test.bind(this, 3, 'Fizz');
  const buzz = test.bind(this, 5, 'Buzz');
  return fizz(buzz(x => x))(n);
}
document.write(Array(100).fill().map((_, i) => fizzbuzz(i + 1)));

Source: https://youtu.be/dC9vdQkU-xI?t=25m9s

It's hard to explain, so let's consider trivial example with only "Fizz" part. I also removed arrow functions and conditional operators, for clarity.

function fizz(n) {
  const fizzer = function(f) {
    if (n % 3 === 0) {
      return function() {
        return 'Fizz' + f('');
      };
    } else {
      return f;
    }
  };
  const identityFunction = function(x) {
    return x;
  };
  return fizzer(identityFunction)(n);
}

In this example, execution can only go in one of two ways:

  • n % 3 !== 0 - we return passed function from fizzer
  • n % 3 === 0 - we get "Fizz", and execution of passed function with empty string

Passed function is identityFunction, therefore in first case we get the number - (n). Second part of second case is optional, but it's where the magic happens when we chain more functions. If you want to better understand what's going on - try removing f('') or changing it to f('Abc') or f. Or try changing last return to return fizzer(fizzer(identityFunction))(n);

\$\endgroup\$
  • \$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please edit it to explain your reasoning (how your solution works and how it improves upon the original) so that everyone can learn from your thought process. \$\endgroup\$ – Toby Speight Jan 10 '18 at 13:22
  • \$\begingroup\$ Edited. Is this sufficient explanation? \$\endgroup\$ – Tymek Jan 10 '18 at 14:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.