Understand Functional Programming basics by (re)writing FizzBuzz
Siddharth
Posted on March 2, 2022
Functional programming is a way of thinking about programs by composing pure functions. It tries to avoid shared state, mutability, and side effects. This makes the code easier to reason about and easier to break apart and use for
other purposes.
Functional programming is declarative, ie. it describes what to do, not how to do it. This makes easier sense to us. (If you want to learn more about the difference between imperative and declarative programming, check out this article).
Function programming is also a bit hard to learn, as most literature related to functional programming can get a bit math-y (because FP was based on lambda calculus).
Let's take a look at Functional Programming by rewriting the classic FizzBuzz in a more functional way.
Wait a minute, pure functions?
Pure functions are functions which
- Given the same input, give the same output
- Have no side effects
/// PURE
const sum = (a, b) => a + b;
sum(1, 2); // 3
sum(1, 2); // still 3
sum(1, 2); // 3s not goin anywhere
/// IMPURE
// impure because the output changes with the same inputs
Math.random(); // 0.21201979699214646
Math.random(); // 0.9585542542409227
Math.random(); // 0.046208832851477144
let g = 1;
// also impure because it mutates state
const setG = x => g = x;
// a harder to spot example:
const doSth = () => {
// we're calling an impure function, so this is also impure.
setG(2);
return g;
}
// exceptions are impure too
const square = x => {
if (x < 0) {
throw new Error('Negative numbers are not allowed');
}
return x * x;
}
// so is logging
console.log('I\'m impure');
So wait, you can't write a program with only pure functions?
Sometimes, we need to have side effects. Most programs can't avoid logging to the console, mutating state or throwing exceptions.
So, we can't write programs with only pure functions. The best we can do is create a clear boundary between the pure and the impure parts of our program, so we can know what to expect.
FizzBuzz?
If you know what FizzBuzz is, you can probably skip this section.
FizzBuzz is a classic programming interview question. All you have to do is write a program that prints numbers from 1 to 100, but replaces multiples of 3 with "Fizz", multiples of 5 with "Buzz", and multiples of both 3 and 5 with "FizzBuzz".
This is the "canonical" FizzBuzz answer:
for (let i = 1; i <= 100; i++) {
if (i % 15 === 0) console.log('FizzBuzz');
else if (i % 3 === 0) console.log('Fizz');
else if (i % 5 === 0) console.log('Buzz');
else console.log(i);
}
In this post, we are going to be rewriting this code in a functional way and exploring its benefits along the way.
Functional FizzBuzz
Abstracting a function
Let's start with the original FizzBuzz code. Can you see anything that could be refactored?
for (let i = 1; i <= 100; i++) {
if (i % 15 === 0) console.log('FizzBuzz');
else if (i % 3 === 0) console.log('Fizz');
else if (i % 5 === 0) console.log('Buzz');
else console.log(i);
}
The first thing which comes to mind is to refactor the divisibility check into a function. We can do that:
const divisible = (x, y) => x % y === 0
for (let i = 1; i <= 100; i++) {
if (divisible(i, 15)) console.log('FizzBuzz');
else if (divisible(i, 3)) console.log('Fizz');
else if (divisible(i, 5)) console.log('Buzz');
else console.log(i);
}
It's more readable now, but there is still room for improvement here. We can
curry the function:
const divisible = x => y => x % y === 0
for (let i = 1; i <= 100; i++) {
const divisibleI = divisible(i); // look ma, a new function with minimal code!
if (divisibleI(15)) console.log('FizzBuzz');
else if (divisibleI(3)) console.log('Fizz');
else if (divisibleI(5)) console.log('Buzz');
else console.log(i);
}
Currying is a technique where a function returns a function. It's a way to pass arguments one at a time.
Here's an example:
const add = x => y => x + y; add(1)(2) // 3
The benefit is that we can now make even more functions out of
add
.const inc = add(1); inc(2) // 3
Currying is a powerful technique that can make your code even more modular.
This makes it trivial to write a function that checks whether i
is divisible by another number.
Cutting out imperative statements
In functional programming, it is discouraged to use imperative statements. Instead, we can replicate them with recursion or other methods.
FizzBuzz is a mapping of numbers to strings. This is exactly what functional programming is about: mapping a value to another value. We don't need a loop here, we just need to map an array of 1 to 100 to an array of "FizzBuzzes"(?).
We can do that by creating a utility function called range
, similar to python's range
function.
const divisible = x => y => x % y === 0
const range = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => min + i)
range(1, 100).map(i => {
const divisibleI = divisible(i);
if (divisibleI(15)) console.log('FizzBuzz');
else if (divisibleI(3)) console.log('Fizz');
else if (divisibleI(5)) console.log('Buzz');
else console.log(i);
});
We can further carve some functions:
const divisible = x => y => x % y === 0
const range = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => min + i)
const map = f => xs => xs.map(f)
const fizzbuzz = i => {
const divisibleI = divisible(i);
if (divisibleI(15)) console.log('FizzBuzz');
else if (divisibleI(3)) console.log('Fizz');
else if (divisibleI(5)) console.log('Buzz');
else console.log(i);
};
const mapFizzbuzz = map(fizzbuzz);
mapFizzbuzz(range(1, 100))
Once again, we used currying to make a reusable function. This made the definition for mapFizzbuzz
extremely simple and clear.
Cutting out the if statements
Right now, the if statements used are quite similar: they are mostly in the form of "if i is divisible by n, the output must include str".
We can refactor these out into an object, and at the same time get rid of all the if statements too!
const divisible = x => y => x % y === 0
const range = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => min + i)
const reduce = f => init => xs => xs.reduce(f, init)
const map = f => xs => xs.map(f)
const CANONICAL_FIZZBUZZ = [
{n: 3, str: 'Fizz'},
{n: 5, str: 'Buzz'},
// {n: 7, str: 'Duzz'} // try this out!
];
const fizzbuzz = keys => i => {
const divisibleI = divisible(i);
const reducer = reduce((acc, {n, str}) => acc + (divisibleI(n) ? str : ''))('');
console.log(reducer(keys) || i);
};
const canonFizzbuzz = fizzbuzz(CANONICAL_FIZZBUZZ);
const mapFizzbuzz = map(canonFizzbuzz);
mapFizzbuzz(range(1, 100))
We can now infinitely extend our FizzBuzz by adding new items to CANONICAL_FIZZBUZZ
. Nice!
Our FizzBuzz is almost complete. But we are missing one rule...
Splitting the pure and impure parts
Right now, we have the impure console.log
sitting right in the middle of our pure fizzbuzz
.
We can cut it out, by making fizzbuzz return the values, and moving the console.log
outside.
This has two benefits:
- The pure and the impure will be cleanly separated.
- We can now reuse the fizzbuzz function in other parts of our code, without having to log the values.
We can do this by returning the values in the fizzbuzz
function, and then using a few more functional utilities to log them:
const divisible = x => y => x % y === 0
const range = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => min + i)
const reduce = f => init => xs => xs.reduce(f, init)
const map = f => xs => xs.map(f)
const forEach = f => xs => xs.forEach(f)
const CANONICAL_FIZZBUZZ = [
{n: 3, str: 'Fizz'},
{n: 5, str: 'Buzz'},
];
const fizzbuzz = keys => i => {
const divisibleI = divisible(i);
const reducer = reduce((acc, {n, str}) => acc + (divisibleI(n) ? str : ''))('');
return reducer(keys) || i;
};
const canonFizzbuzz = fizzbuzz(CANONICAL_FIZZBUZZ);
const mapFizzbuzz = map(canonFizzbuzz);
// IMPURE CODE STARTS HERE
const print = x => console.log(x)
const printEach = forEach(print);
printEach(mapFizzbuzz(range(1, 100)))
Whew.
We're done!
That's it! I'm hoping you've got a feel for functional programming. Are you going to use functional programming in your next project? Or will you stick with OOP (or some other dialect)? Let me know!
Posted on March 2, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.