Last active
November 28, 2017 01:31
-
-
Save hrb90/15da8754c5f8976aa5477358df247ec7 to your computer and use it in GitHub Desktop.
FizzBuzz with monads!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*** | |
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)) | |
); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A couple helper function suggestions :-)