วงแหวนเว็บ

neizod's speculation

insufficient data for meaningful answer

FizzBuzz and Composite Function in Haskell

FizzBuzz might be the most popular basic programming interview problem (knowing only syntax but not advance algorithm). It ask only the output transformation of an integer. The rule is super simple: if the number is divided by three, answer “Fizz”, divided by five, answer “Buzz”, divided by both then “FizzBuzz”, otherwise just return the number as-is.

number 1 2 3 4 5 6 7 14 15 16
answer 1 2 Fizz 4 Buzz Fizz 7 14 FizzBuzz 16

We may code it in some form of the simple if-else, like this

fizzBuzz number
  | mod number 15 == 0 = "FizzBuzz"
  | mod number  3 == 0 = "Fizz"
  | mod number  5 == 0 = "Buzz"
  | otherwise          = show number

And that’s it… However, why stop there? Recalled that Haskell have the concept of composite function, like in the real mathematics, that let us define function without parameter! What will FizzBuzz that employed that technique will looks like?


After some walk, I just realize that the composite function is just a passing intermediate results. That is we will lose the information of the first parameter number. To fix this, we have to duplicate number multiple times and passing it down the factory line (some get changed and some don’t). And finally, we just combine all of the results…

Sound familiar? That is the old friend map-reduce! Except this time we swap map’s arguments, so it acted on the single data with many different functions instead.

Thus, the code should be in the form of

fizzBuzz number = reduce f0 (map (`id` number) [f1, f2, f3, ...])

Since we also want the final function to be the composite function, we’ll nudge the variable number to the rightmost position on the RHS. Which might be done via flip, like this

fizzBuzz number = reduce f0 (flip map [f1, f2, f3, ...] (`id` number))

It’s the matter of dealing with parenthesis. That is we can use composite function to change its look and make the variable number free.

fizzBuzz number = reduce f0 . flip map [f1, f2, f3, ...] . flip id $ number

And voilà! Now we may remove number from both side of the equation

fizzBuzz = reduce f0 . flip map [f1, f2, f3, ...] . flip id

Cut-in spoiler: when we look in to the future, we’ll see that the technique of reversed mapping is something that keep reoccurring. Thus it is not a bad idea to define this concept as a function too! That is we might substitute some parts of the above equation with

distribute fs = flip map fs . flip id

Or defined the function in the way that not require parameters

distribute = flip (.) (flip id) . flip map

So now it’s time to design the reduce function (and corresponding functions). One of the possible design might looks like this

f1 :: Int -> String -- output Fizz, Buzz, or FizzBuzz; according to rules.
                    -- otherwise output empty string.
f2 :: Int -> String -- output the number in string (acted as default case).
f0 :: [String] -> String -- output first non-empty string, in other words:
                         -- output f1 if it has some value, otherwise f2.
fizzBuzz = reduce f0 . distribute [f1, f2]

The most complicate function for mapping must be f1. We’ll start by considering only the case of divided by three (the Fizz). Which might be done via

f1 n = (["Fizz",""] !!) $ fromEnum $ not $ (== 0) $ (mod n 3)

We’ll refrain from ctrl-c ctrl-v the code to let it handle Buzz. Instead, we’ll do

say word m n = ([word,""] !!) $ fromEnum $ not $ (== 0) $ (mod n m)
f1 = foldr1 (++) . distribute [say "Fizz" 3, say "Buzz" 5]

Observe that at the end of say it use mod, which is a function that takes two arguments. But to use composite function, all intermediate functions must have an interface of one parameter… Thus the curry save the day (then uncurry later).

index = fromEnum . not . (== 0)
say word = curry (([word,""] !!) . index . uncurry (flip mod))

To eliminate the variable word, we use flip technique like before

say = curry . flip (.) (index . uncurry (flip mod)) . (!!) . (:[""])

Congratulations reaching here! The rest is just a walk in the garden. Since, you may already guess that, reduce f0 is just head . dropWhile null. And f2 is the built-in show.

Thus the final code that are parameter free is

distribute :: [a -> b] -> a -> [b]
distribute = flip (.) (flip id) . flip map

index :: Int -> Int
index = fromEnum . not . (== 0)

say :: String -> Int -> Int -> String
say = curry . flip (.) (index . uncurry (flip mod)) . (!!) . (:[""])

rules :: Int -> String
rules = foldr1 (++) . distribute [say "Fizz" 3, say "Buzz" 5]

fizzBuzz :: Int -> String
fizzBuzz = head . dropWhile null . distribute [rules, show]

Um… why going the extra mile 😂

P.S. it just easy to append rules list, like by adding say "Up" 7, some answer became

number 7 21 35 105
answer Up FizzUp BuzzUp FizzBuzzUp

neizod

author