FizzBuzz fun in Scala: Combining functions
Every implementation of FizzBuzz in this series, at its core, has relied on an infinitely counting lazy list. This modelling is logical, as the game can theoretically be played indefinitely. In this post, we will explore the possibility of defining a single function operating on this running number and, as a bonus, utilize Scala 3's extension methods to enhance code clarity.
LazyList
.from(1)
... // multiple statements
.take(n)
.toList
What we are looking for is:
LazyList
.from(1)
.map(i: Int => ???)
.take(n)
.toList
Let's define a method and its signature:
LazyList
.from(1)
.map(fizzbuzzAt)
.take(n)
.toList
def fizzbuzzAt(n: Int): String = ???
Note from the first article in this series an implementation of fizzbuzzAt
using if
statements could be:
// Commenting the Scala 3 version / Scala CLI directive
//> using scala 3.3.1
@main def fizzbuzz(): Unit =
// function to re-implement
def fizzbuzzAt(n: Int): String =
if n % 15 == 0 then "FizzBuzz"
else if n % 5 == 0 then "Buzz"
else if n % 3 == 0 then "Fizz"
else
n.toString
end fizzbuzzAt
def fizzbuzz(n: Int): List[String] =
LazyList
.from(1)
.map(fizzbuzzAt)
.take(n)
.toList
end fizzbuzz
fizzbuzz(20).foreach(fb => print(fb + ","))
end fizzbuzz
Now we want to simplify the if
expression and make it easier to implement e.g. other 'shouts', say Bazz every 2 times. For this article, we want to be looking into a solution using functions.
A quite natural function to decide whether Fizz should be shouted is one taking an integer as input and returning maybe a String ("Fizz"):
val fizz: Int => Option[String] = ???
Our fizzbuzzAt
could then look like:
def fizzbuzzAt(n: Int): String =
// fizz and buzz are similar and can be implemented:
val fizz: Int => Option[String] = i =>
if i % 3 == 0 then Some("Fizz") else None
val buzz: Int => Option[String] =
i => if i % 5 == 0 then Some("Buzz") else None
// Both need to be combined into a function with same signature
val combined: Int => Option[String] = ???
// The final function which should return a String
val fizzbuzz: Int => String = ???
fizzbuzz(n)
end fizzbuzzAt
So we need to combine 2 functions of the same signature Int => Option[String]
:
def combine[A](f1: A => Option[String], f2: A => Option[String]): A => Option[String] =
a =>
(f1(a), f2(a)) match
case (Some(s1), Some(s2)) => Some(s1 + s2)
case (None, Some(s2)) => Some(s2)
case (Some(s1), None) => Some(s1)
case (None, None) => None
We generalized a little in the argument, the return type we keep at String as we need to operate on it. (In a next blog post we will also generalize this more but we leave it as this for now.)
fizzbuzz
needs to turn the combined function Int => Option[String]
into a function Int => String
. For the FizzBuzz game, turning Option[String]
into a String
given an integer can be done like:
val fizzbuzz: Int => String = i =>
combined(i).getOrElse(i.toString)
fizzbuzzAt
then becomes:
def fizzbuzzAt(n: Int): String =
val fizz: Int => Option[String] = i =>
if i % 3 == 0 then Some("Fizz") else None
val buzz: Int => Option[String] = i =>
if i % 5 == 0 then Some("Buzz") else None
val combined: Int => Option[String] =
combine(fizz, buzz)
val fizzbuzz: Int => String = i =>
combined(i).getOrElse(i.toString)
fizzbuzz(n)
end fizzbuzzAt
Let's transform fizzbuzzAt
into a function as well:
val fizzbuzzAt: Int => String =
val fizz: Int => Option[String] = i =>
if i % 3 == 0 then Some("Fizz") else None
val buzz: Int => Option[String] = i =>
if i % 5 == 0 then Some("Buzz") else None
val combined: Int => Option[String] =
combine(fizz, buzz)
val fizzbuzz: Int => String = i =>
combined(i).getOrElse(i.toString)
fizzbuzz // return a function from Int to String
end fizzbuzzAt
Let's eliminate the duplication in the fizz
and buzz
functions:
val fizzbuzzAt: Int => String =
extension (word: String)
def every(n: Int): Int => Option[String] = i =>
if i % n == 0 then Some(word) else None
val fizz = "Fizz".every(3)
val buzz = "Buzz".every(5)
val combined: Int => Option[String] =
combine(fizz, buzz)
val fizzbuzz: Int => String = i =>
combined(i).getOrElse(i.toString)
fizzbuzz
end fizzbuzzAt
Here we make use of a Scala 3 extension method to make the function definition clearer and, I would argue, more easily reusable with less chance of errors.
Putting all word shouts into a list allows for easier extension in a single place:
val fizzbuzzAt: Int => String =
extension (word: String)
def every(n: Int): Int => Option[String] = i => if i % n == 0 then Some(word) else None
val wordShouts = List(
"Fizz".every(3),
"Buzz".every(5),
// Easy to extend:
"Bazz".every(2)
)
val combined = wordShouts.reduce(combine) // will fail on empty wordShouts
val fizzbuzz: Int => String = i =>
combined(i).getOrElse(i.toString)
fizzbuzz
end fizzbuzzAt
If we want to account for empty wordShouts
we can instead resort to a fold
:
val combined = words.fold(_ => None)(combine)
We defined the 'zero' function to accept any integer and return None
. I will expand on this in a next blog post and leave the reduce
function in the final version of this article:
// Commenting the Scala 3 version / Scala CLI directive
//> using scala 3.3.1
@main def fizzbuzz(): Unit =
def combine[A](f1: A => Option[String], f2: A => Option[String]): A => Option[String] =
a =>
(f1(a), f2(a)) match
case (Some(s1), Some(s2)) => Some(s1 + s2)
case (None, Some(s2)) => Some(s2)
case (Some(s1), None) => Some(s1)
case (None, None) => None
val fizzbuzzAt: Int => String =
extension (word: String)
def every(n: Int): Int => Option[String] = i =>
if i % n == 0 then Some(word) else None
val wordShouts = List(
"Fizz".every(3),
"Buzz".every(5)
)
val combined = wordShouts.reduce(combine) // will fail on empty wordShouts
val fizzbuzz: Int => String = i =>
combined(i).getOrElse(i.toString)
fizzbuzz
end fizzbuzzAt
def fizzbuzz(n: Int): List[String] =
LazyList
.from(1)
.map(fizzbuzzAt)
.take(n)
.toList
end fizzbuzz
fizzbuzz(20).foreach(fb => print(fb + ","))
end fizzbuzz
This prints:
1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz,16,17,Fizz,19,Buzz,
In conclusion, this article demonstrated how to create a single function for the FizzBuzz game using Scala, combining functions and making use of Scala 3's extension methods for clarity and reduced potential for errors. This approach allows for easier testing and extension of the game with additional word shouts, showcasing the power and flexibility of functional programming and Scala.