loading...

Monoidal FizzBuzz

aibhstin profile image Aibhstin ・2 min read

I first found out about the ‘FizzBuzz’ interview test from a Tom Scott video. In it, he lays out the main concepts of the problem, and the slightly hidden difficulty in that the kind of branching the FizzBuzz problem requires isn’t instantly translatable into code. Tom Scott codes his solution in JavaScript but I always found a small sense of pride in that, in Haskell, the problem kind of is translatable directly into a function, using guarded equations:

fizzBuzz :: Int -> String
fizzBuzz n
  | div3 && div5 = "FizzBuzz\n"
  | div3 = "Fizz\n"
  | div5 = "Buzz\n"
  | otherwise = show n ++ "\n"
  where div3 = n `rem` 3 == 0
        div5 = n `rem` 5 == 0

Initially, back when I was just learning to program and still hadn’t even heard of monoids or monads, this seemed like a stellar solution. Printing out the full solution to the FizzBuzz solution was then done like so:

main :: IO ()
main = do
  mapM_ putStrLn $
    fmap fizzBuzz [1..100]

(Note: I had no idea at the time how mapM_ worked, I just knew it did.)

There is a glaring problem with this solution however: adding new conditions. Say for example I want to add the case for a number being a factor of 7 and add the string “Foo”, I would have to alter the fizzBuzz function to something like this:

fizzBuzz :: Int -> String
fizzBuzz n
  | div3 && div5 && div7 = "FizzBuzzFoo"
  | div3 && div5 = "FizzBuzz"
  | div3 && div7 = "FizzFoo"
  | div5 && div7 = "BuzzFoo"
  | div3 = "Fizz"
  | div5 = "Buzz"
  | div7 = "Foo"
  | otherwise = show n ++ "\n"
  where div3 = n `rem` 3 == 0
        div5 = n `rem` 5 == 0
        div7 = n `rem` 7 == 0

This solution quickly grows to be unwieldy and becomes harder to maintain and manage. But this is Haskell, and we can make a much nicer solution by spotting opportunities for abstraction. Specifically, let’s look at the Monoid typeclass and what types it is defined for:

GHCi> :i Monoid
class Semigroup a => Monoid a where
  mempty :: a
  mappend :: a -> a -> a
  mconcat :: [a] -> a
. . .
instance Monoid b => Monoid (a -> b)
. . .

There’s one particular instance of Monoid that sticks out, for the type (a -> b) where b has an instance of Monoid defined. Let’s define a function to take a number and return “Fizz” if it is divisible by 3:

div3 :: Int -> String
div3 n = if n `rem` 3 == 0 
         then "Fizz" 
         else ""

The function has the type (Int -> String), and since String has an instance of Monoid, then FizzBuzz mini-functions like div3 must also have an instance of Monoid. This means we can clean up our entire FizzBuzz function to use monoids instead, like this:

fizzBuzz :: Int -> String
fizzBuzz n = if length str == 0
             then show n
             else str
             where str = go n
                   go = mconcat [ div3
                                , div5
                                , div7]

div3 n = if n `rem` 3 == 0 then "Fizz" else ""
div5 n = if n `rem` 5 == 0 then "Buzz" else ""
div7 n = if n `rem` 7 == 0 then "Foo" else ""

Now adding a new condition is as easy as defining it as a new function and simply adding it to the list!

Posted on by:

aibhstin profile

Aibhstin

@aibhstin

I'm an Ethical Hacking & Cybersecurity student and a Haskell programmer.

Discussion

markdown guide
 

This is a great example of how to use a monoid. It requires some previous knowledge of monoids to understand, but for me this was perfect. It was interesting seeing how you reasoned about using a monoid. I'd love to see more like this.

 

Thank you! I really enjoy writing about the practical uses of Haskell's typeclasses. I'll definitely write more.