Simplifying if-complexity in FizzBuzz
In this series, I've mentioned that using an if
-expression in the FizzBuzz problem can be more error-prone and complex compared to functional approaches. In this brief article, I'll demonstrate why that's the case.
Let's start with a simple working implementation using if
s:
// Commenting the Scala 3 version / Scala CLI directive
//> using scala 3.3.1
def fizzbuzz(s: Int): List[String] =
def shout(i: Int): String =
if i % 15 == 0 then "FizzBuzz"
else if i % 3 == 0 then "Fizz"
else if i % 5 == 0 then "Buzz"
else
i.toString
val fb: LazyList[String] = LazyList.from(1).map(shout)
(fb take s).toList
end fizzbuzz
@main def fizzbuzz(): Unit =
fizzbuzz(40).foreach(fb => print(fb + ","))
end fizzbuzz
Suppose the requirement is now amended to include printing "Bazz" for every even number, such as printing "FizzBuzzBazz" at n = 30.
// Commenting the Scala 3 version / Scala CLI directive
//> using scala 3.3.1
def fizzbuzz(s: Int): List[String] =
def shout(i: Int): String =
if i % 30 == 0 then "FizzBuzzBazz"
else if i % 15 == 0 then "FizzBuzz"
else if i % 10 == 0 then "BuzzBazz"
else if i % 6 == 0 then "FizzBazz"
else if i % 5 == 0 then "Buzz"
else if i % 3 == 0 then "Fizz"
else if i % 2 == 0 then "Bazz"
else
i.toString
val fb: LazyList[String] = LazyList.from(1).map(shout)
(fb take s).toList
end fizzbuzz
@main def fizzbuzz(): Unit =
fizzbuzz(40).foreach(fb => print(fb + ","))
end fizzbuzz
It is clear that adding more words to this type of if-expression will cause it to expand rapidly and become increasingly difficult to get right. This is what I meant when I said extending the if
-expression is error-prone.
Of course, this if
-expression is just the simplest and most commonly chosen solution by developers (I admit we've used this exercise as a hiring question as well), but that doesn't mean it can't be improved further, starting from its current state. Let's explore how to enhance it, beginning with using a mutable collection in the shout
function:
def shout(i: Int): String =
val sb = StringBuilder("")
if i % 3 == 0 then sb.append("Fizz")
if i % 5 == 0 then sb.append("Buzz")
if i % 2 == 0 then sb.append("Bazz")
if sb.isEmpty then i.toString
else sb.toString()
end shout
Since the StringBuilder
is local to the shout
function, it doesn't break referential transparency, so I don't mind. However, what bothers me a bit more are the if
-statements that are no longer expressions, meaning they don't resolve to a value which is used further.
Let's improve by defining a method with an explicit Unit
return type. The fact that a method returns Unit
indicates it is performing a side effect.
def shout(i: Int): String =
val sb = StringBuilder("")
def maybeAppend(word: String, turn: Int): Unit =
if i % turn == 0 then sb.append(word) else ()
maybeAppend("Fizz", 3)
maybeAppend("Buzz", 5)
maybeAppend("Bazz", 2)
if sb.isEmpty then i.toString
else sb.toString()
end shout
Putting the word-turn combination in a List
improves even more:
def shout(i: Int): String =
val sb = StringBuilder("")
def maybeAppend(word: String, turn: Int): Unit =
if i % turn == 0 then sb.append(word) else ()
List(("Fizz", 3), ("Buzz", 5), ("Bazz", 2)).foreach(maybeAppend)
if sb.isEmpty then i.toString
else sb.toString()
end shout
At this point inlining the maybeAppend
method again is probably more clear:
def shout(i: Int): String =
val sb = StringBuilder("")
List(("Fizz", 3), ("Buzz", 5), ("Bazz", 2)).foreach:
(word: String, turn: Int) =>
if i % turn == 0 then sb.append(word) else ()
if sb.isEmpty then i.toString
else sb.toString()
end shout
As a final step in this article, we can then get rid of the mutable collection by using a foldLeft
:
def shout(i: Int): String =
val words = List(("Fizz", 3), ("Buzz", 5), ("Bazz", 2))
.foldLeft(""):
case (acc, (word: String, turn: Int)) =>
if i % turn == 0 then acc + word else acc
end words
if words.isBlank then i.toString
else words
end shout
The complete implementation becomes then:
// Commenting the Scala 3 version / Scala CLI directive
//> using scala 3.3.1
def fizzbuzz(s: Int): List[String] =
def shout(i: Int): String =
val words = List(("Fizz", 3), ("Buzz", 5), ("Bazz", 2))
.foldLeft(""):
case (acc, (word: String, turn: Int)) =>
if i % turn == 0 then acc + word else acc
end words
if words.isBlank then i.toString
else words
end shout
val fb: LazyList[String] = LazyList.from(1).map(shout)
(fb take s).toList
end fizzbuzz
@main def fizzbuzz(): Unit =
fizzbuzz(40).foreach(fb => print(fb + ","))
end fizzbuzz
Extending this solution to even more FizzBuzz words is now as simple as adding an element to a list while the implementation only uses simple functions and is relatively easy to understand.
We began this article by demonstrating that an initial if-expression-based solution for the FizzBuzz problem can be error-prone and complicated when expanded. Through step-by-step refactoring, we arrived at a solution using functional programming constructs. Our final solution is more future-proof and easily understandable, allowing for extensions with minimal effort.