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!