4
\$\begingroup\$

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?

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

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 vars could be consts. 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.

\$\endgroup\$
1
  • \$\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
    Commented Jan 4, 2016 at 8:15
3
\$\begingroup\$

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 the mappers 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
\$\endgroup\$
1
  • \$\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
    Commented Jan 4, 2016 at 8:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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