Skip to content

Instantly share code, notes, and snippets.

@hrb90
Last active November 28, 2017 01:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hrb90/15da8754c5f8976aa5477358df247ec7 to your computer and use it in GitHub Desktop.
Save hrb90/15da8754c5f8976aa5477358df247ec7 to your computer and use it in GitHub Desktop.
FizzBuzz with monads!
/***
You've probably seen FizzBuzz before. If you need a reminder, the problem is
this: Print out a list of numbers from 1 to 100, except that if a number's
divisible by 3, replace it with "Fizz", and if it's divisible by 5, replace it
with "Buzz". If it's divisible by 3 and 5, replace it with "FizzBuzz".
This is pretty straightforward to do in JavaScript with a for loop and some
if-then blocks, but it quickly gets more complicated. What if we want to add
"Quux" at the end of every number divisible by 7? And also add "Prime" at the
beginning of every prime number? We'd like a *composable* solution, one where
we can put the pieces of the problem together like Lego blocks.
We'll use a pattern from functional programming called a Writer monad to
implement such a solution. Maybe you've heard of monads before and found them
intimidating, but I promise this won't be. It'll take us less than 50 lines of
code without comments, and all you need to know is ES6 Javascript.
Our implementation of Writer is simply a JavaScript object with three fields:
- data holds the data we are transforming
- log holds a string
- bind is more complicated: it's a higher-order function, which means that it
takes a function as input. That function should return a new Writer if
passed our data. bind calls the function it's passed on our data, and
returns a new Writer by extracting the data from the returned Writer,
and appending the log of the returned Writer to our log.
An example might help:
pure(3).bind(x => { data: x * x, log: "square " })
.bind(x => { data: x + 1, log: "add1 " })
returns a Writer monad with data 10 and log "square add1"
The first thing we'll do is figure out how to make a new Writer from an old
Writer's data and log and a function we passed to bind.
I've named this function makeBind, which is a little tricky! The thing to keep
in mind is that this function is *curried*; when we call it with the first pair
of arguments (the data and log) it returns a function, which becomes the new
Writer's bind method.
***/
const makeBind = (prevData, prevLog) => fn => {
const newData = fn(prevData).data;
const newLog = prevLog + fn(prevData).log;
return {
data: newData,
log: newLog,
bind: makeBind(newData, newLog)
};
};
/***
We also have a function called pure, which wraps some data up in a Writer with
an empty log...
***/
const pure = a => ({
data: a,
log: "",
bind: makeBind(a, "")
});
/***
...and a function withLog, which takes some data and a string and returns
a new Writer wrapping the data with the given string as the log.
***/
const withLog = (a, stuffToWrite) => ({
data: a,
log: stuffToWrite,
bind: makeBind(a, stuffToWrite)
});
/***
This is a helper function that takes a test (like "is this number divisible by
3?") and a thing to write (like "Fizz") and returns a function that we can pass
to our Writer's bind method
***/
const makeFizzBuzzPiece = (test, thingToWrite) => n =>
test(n) ? withLog(n, thingToWrite) : pure(n);
/***
These are the monadic pieces we are using to implement our FizzBuzz solution.
They are the things we'll pass to a Writer's bind method.
***/
const fizzer = makeFizzBuzzPiece(n => n % 3 === 0, "Fizz");
const buzzer = makeFizzBuzzPiece(n => n % 5 === 0, "Buzz");
const quuxer = makeFizzBuzzPiece(n => n % 7 === 0, "Quux");
// Just a little helper function. We'll need it to test for primality.
const range = (low, high) =>
Array(high - low + 1).fill(0).map((_, i) => i + low);
// The primality test here is tricky! This is another monadic piece.
const primer = makeFizzBuzzPiece(
n => n > 1 && range(2, n - 1).every(divisor => n % divisor !== 0),
"Prime"
);
/***
A helper function to get from the Writer to what FizzBuzz wants us to print.
If we've written anything to the log, we take the log and join it together into
one word. Otherwise, we take the data.
***/
const fbExtract = ({ data, log }) => (log === "" ? data : log);
// Put it all together! For "FizzBuzz Classic", you just need to remove the
// .bind(primer) and .bind(quuxer).
range(1, 100).forEach(x => {
console.log(
fbExtract(pure(x).bind(primer).bind(fizzer).bind(buzzer).bind(quuxer))
);
});
@timbuckley
Copy link

timbuckley commented Aug 11, 2017

A couple helper function suggestions :-)

const range = (low, high) => Array(high - low + 1).fill(0).map((_,i) => i + low)

const fbExtract = ({ data, log }) => log === '' ? data : log

@hrb90
Copy link
Author

hrb90 commented Aug 11, 2017

Good improvements!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment