I was playing around with ES6 arrow functions, currying and partial application and I decided to try a functional implementation of the FizzBuzz problem:
function* range(start, stop) {
for (var i = start; i < stop; i++)
yield i
}
var map = fn => res => x => fn(x) ? res : null
var isDividedBy = dividend => divisor => divisor % dividend === 0
var mapDividedBy = x => map(isDividedBy(x))
var mappers = [
mapDividedBy(3)('Fizz'),
mapDividedBy(5)('Buzz')
]
var applyMappers = mappers => x => mappers.map(fn => fn(x)).join('') || x
var result = Array.from(range(1, 100)).map(applyMappers(mappers))
console.log(result)
My main focus was to be able to easily add any other case like "print Zazz for numbers lower than 10" by simply adding map(x => x < 10)('Zazz')
to the mappers
array.
What do you think?
2 Answers 2
map
vs if
var map = fn => res => x => fn(x) ? res : null
I would rename this to something more like ifThen
or when
-- calling this function map
is confusing, to me at least, given the normal usage of map
. This is an if
where the else
is always null
.
In a larger program you may find that providing the true/false results as functions instead of constants would be more powerful, but here that would just add noise.
const
Most of your var
s could be const
s. Doesn't make much difference here but since you're deliberately being functional const
would help declare your intent.
without mapDividedBy
I don't think you need this specialized function. Just pass the predicate and true-result into your when
/if
:
const when = pred => trueRes => x => pred(x) ? trueRes : null
const isDividedBy = dividend => divisor => divisor % dividend === 0
const mappers = [
when(isDividedBy(3))('Fizz'),
when(isDividedBy(5))('Buzz'),
]
naming
I'm not sure mappers
is the most descriptive name. I can't think of anything better at present, so that's not very helpful, but any (pure) function is just a "mapper" from inputs to output(s). Maybe tests
? Hmm.
In any event, since this is a FizzBuzz I suggest providing a final function named as such, for convenience and readability:
const fizzbuzz = applyMappers(mappers)
const result = Array.from(range(1, 100)).map(fizzbuzz)
misc
I like your currying approach.
Clojure has a function juxt
which does the same thing as mappers.map(fn => fn(x))
but looks a little cleaner, being a separate function:
const fizzbuzz = x => juxt(mappers)(x).join('') || x
But whether this is more readable or more obscure depends on whether you know Clojure, so I've left this point to the end.
-
\$\begingroup\$ I really like your suggestions, especially the ones about naming. This was a quick exercise so I didn't put too much thought into it and "Naming things is one of the two hard problems in programming". \$\endgroup\$david.s– david.s2016年01月04日 08:15:00 +00:00Commented Jan 4, 2016 at 8:15
You've got a nice idea, planning for extensibility. There is one issue that I see with the way you've implemented it.
My main focus was to be able to easily add any other case like "print Zazz for numbers lower than 10" by simply adding
map(x => x < 10)('Zazz')
to themappers
array.
This won't actually work. It'll print something different depending on where in the mappers
array you put it, but if you append the rule, it'll return this for 1 to 5:
Zazz
Zazz
FizzZazz
Zazz
BuzzZazz
What's happening is that you are avoiding having to check for the mod 15 case by combining the mod 3 and mod 5 cases. This is fine if that's all you'll ever have to worry about, but if you intend to add cases later it's better to be explicit about things so you can be more flexible in your later rules.
These two changes will replace un-modified numbers with 'Zazz':
var mappers = [
mapDividedBy(15)('FizzBuzz'),
mapDividedBy(3)('Fizz'),
mapDividedBy(5)('Buzz'),
map(x => x < 10)('Zazz')
]
var applyMappers = mappers => x => mappers.map(fn => fn(x)).filter(x => x).shift() || x
-
\$\begingroup\$ Just as I was posting this I realized that I basically settled on the convention that all rulles are applied in order and their results are concatenated. I continued my little exercise by introducing the concepts of rule precedence and combining rules using AND/OR. But, since this was FizzBuzz to start with, I didn't want to explain all these new rules and really complicate things. \$\endgroup\$david.s– david.s2016年01月04日 08:31:12 +00:00Commented Jan 4, 2016 at 8:31
Explore related questions
See similar questions with these tags.